@actuate-media/cms-admin 0.8.0 → 0.8.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 (433) hide show
  1. package/dist/AdminRoot.d.ts.map +1 -1
  2. package/dist/AdminRoot.js +44 -42
  3. package/dist/AdminRoot.js.map +1 -1
  4. package/dist/__tests__/lib/search.test.js +10 -10
  5. package/dist/__tests__/lib/search.test.js.map +1 -1
  6. package/dist/__tests__/lib/utils.test.js.map +1 -1
  7. package/dist/__tests__/router/match-route.test.js.map +1 -1
  8. package/dist/__tests__/router/strip-base.test.js.map +1 -1
  9. package/dist/actuate-admin.css +1 -1
  10. package/dist/components/Breadcrumbs.d.ts.map +1 -1
  11. package/dist/components/Breadcrumbs.js +2 -4
  12. package/dist/components/Breadcrumbs.js.map +1 -1
  13. package/dist/components/CommandPalette.d.ts.map +1 -1
  14. package/dist/components/CommandPalette.js +7 -3
  15. package/dist/components/CommandPalette.js.map +1 -1
  16. package/dist/components/ContentOverviewChart.d.ts.map +1 -1
  17. package/dist/components/ContentOverviewChart.js.map +1 -1
  18. package/dist/components/ErrorBoundary.d.ts.map +1 -1
  19. package/dist/components/ErrorBoundary.js.map +1 -1
  20. package/dist/components/FocalPointPicker.d.ts.map +1 -1
  21. package/dist/components/FocalPointPicker.js +4 -2
  22. package/dist/components/FocalPointPicker.js.map +1 -1
  23. package/dist/components/FolderTree.d.ts.map +1 -1
  24. package/dist/components/FolderTree.js +18 -10
  25. package/dist/components/FolderTree.js.map +1 -1
  26. package/dist/components/LivePreview.d.ts +1 -1
  27. package/dist/components/LivePreview.d.ts.map +1 -1
  28. package/dist/components/LivePreview.js +6 -2
  29. package/dist/components/LivePreview.js.map +1 -1
  30. package/dist/components/LocaleProvider.d.ts.map +1 -1
  31. package/dist/components/LocaleProvider.js.map +1 -1
  32. package/dist/components/LocaleSwitcher.d.ts.map +1 -1
  33. package/dist/components/LocaleSwitcher.js +1 -1
  34. package/dist/components/LocaleSwitcher.js.map +1 -1
  35. package/dist/components/MediaPickerModal.d.ts.map +1 -1
  36. package/dist/components/MediaPickerModal.js.map +1 -1
  37. package/dist/components/PresenceIndicator.d.ts.map +1 -1
  38. package/dist/components/PresenceIndicator.js +5 -2
  39. package/dist/components/PresenceIndicator.js.map +1 -1
  40. package/dist/components/SEOPanel.d.ts +1 -1
  41. package/dist/components/SEOPanel.d.ts.map +1 -1
  42. package/dist/components/SEOPanel.js +110 -24
  43. package/dist/components/SEOPanel.js.map +1 -1
  44. package/dist/components/SEOPerformance.d.ts.map +1 -1
  45. package/dist/components/SEOPerformance.js +2 -2
  46. package/dist/components/SEOPerformance.js.map +1 -1
  47. package/dist/components/ThemeProvider.d.ts.map +1 -1
  48. package/dist/components/ThemeProvider.js.map +1 -1
  49. package/dist/components/TipTapEditor.d.ts.map +1 -1
  50. package/dist/components/TipTapEditor.js +5 -1
  51. package/dist/components/TipTapEditor.js.map +1 -1
  52. package/dist/components/VersionHistory.d.ts +1 -1
  53. package/dist/components/VersionHistory.d.ts.map +1 -1
  54. package/dist/components/VersionHistory.js +1 -1
  55. package/dist/components/VersionHistory.js.map +1 -1
  56. package/dist/components/ui/Avatar.d.ts.map +1 -1
  57. package/dist/components/ui/Avatar.js.map +1 -1
  58. package/dist/components/ui/Badge.d.ts.map +1 -1
  59. package/dist/components/ui/Badge.js.map +1 -1
  60. package/dist/components/ui/Button.d.ts.map +1 -1
  61. package/dist/components/ui/Button.js.map +1 -1
  62. package/dist/components/ui/CommandPalette.d.ts.map +1 -1
  63. package/dist/components/ui/CommandPalette.js +8 -2
  64. package/dist/components/ui/CommandPalette.js.map +1 -1
  65. package/dist/components/ui/ConfirmDialog.d.ts.map +1 -1
  66. package/dist/components/ui/ConfirmDialog.js.map +1 -1
  67. package/dist/components/ui/DataTable.d.ts.map +1 -1
  68. package/dist/components/ui/DataTable.js +1 -3
  69. package/dist/components/ui/DataTable.js.map +1 -1
  70. package/dist/components/ui/EmptyState.d.ts.map +1 -1
  71. package/dist/components/ui/EmptyState.js +1 -1
  72. package/dist/components/ui/EmptyState.js.map +1 -1
  73. package/dist/components/ui/Modal.d.ts.map +1 -1
  74. package/dist/components/ui/Modal.js.map +1 -1
  75. package/dist/components/ui/Pagination.d.ts +1 -1
  76. package/dist/components/ui/Pagination.d.ts.map +1 -1
  77. package/dist/components/ui/Pagination.js +7 -2
  78. package/dist/components/ui/Pagination.js.map +1 -1
  79. package/dist/components/ui/SearchInput.d.ts.map +1 -1
  80. package/dist/components/ui/SearchInput.js.map +1 -1
  81. package/dist/components/ui/Skeleton.d.ts.map +1 -1
  82. package/dist/components/ui/Skeleton.js.map +1 -1
  83. package/dist/components/ui/Toast.d.ts.map +1 -1
  84. package/dist/components/ui/Toast.js.map +1 -1
  85. package/dist/components/ui/index.d.ts.map +1 -1
  86. package/dist/components/ui/index.js.map +1 -1
  87. package/dist/fields/ArrayField.d.ts.map +1 -1
  88. package/dist/fields/ArrayField.js +1 -1
  89. package/dist/fields/ArrayField.js.map +1 -1
  90. package/dist/fields/BlockBuilderField.d.ts.map +1 -1
  91. package/dist/fields/BlockBuilderField.js +7 -7
  92. package/dist/fields/BlockBuilderField.js.map +1 -1
  93. package/dist/fields/DateField.d.ts.map +1 -1
  94. package/dist/fields/DateField.js +1 -1
  95. package/dist/fields/DateField.js.map +1 -1
  96. package/dist/fields/FieldRenderer.d.ts.map +1 -1
  97. package/dist/fields/FieldRenderer.js.map +1 -1
  98. package/dist/fields/GroupField.d.ts.map +1 -1
  99. package/dist/fields/GroupField.js +1 -1
  100. package/dist/fields/GroupField.js.map +1 -1
  101. package/dist/fields/MediaField.d.ts.map +1 -1
  102. package/dist/fields/MediaField.js +1 -1
  103. package/dist/fields/MediaField.js.map +1 -1
  104. package/dist/fields/NavBuilderField.d.ts.map +1 -1
  105. package/dist/fields/NavBuilderField.js +2 -5
  106. package/dist/fields/NavBuilderField.js.map +1 -1
  107. package/dist/fields/NumberField.d.ts +1 -1
  108. package/dist/fields/NumberField.d.ts.map +1 -1
  109. package/dist/fields/NumberField.js +2 -2
  110. package/dist/fields/NumberField.js.map +1 -1
  111. package/dist/fields/RelationshipField.d.ts.map +1 -1
  112. package/dist/fields/RelationshipField.js +7 -3
  113. package/dist/fields/RelationshipField.js.map +1 -1
  114. package/dist/fields/RichTextField.d.ts +1 -1
  115. package/dist/fields/RichTextField.d.ts.map +1 -1
  116. package/dist/fields/RichTextField.js +2 -2
  117. package/dist/fields/RichTextField.js.map +1 -1
  118. package/dist/fields/SelectField.d.ts.map +1 -1
  119. package/dist/fields/SelectField.js +9 -7
  120. package/dist/fields/SelectField.js.map +1 -1
  121. package/dist/fields/SlugField.d.ts.map +1 -1
  122. package/dist/fields/SlugField.js +1 -1
  123. package/dist/fields/SlugField.js.map +1 -1
  124. package/dist/fields/TextField.d.ts +1 -1
  125. package/dist/fields/TextField.d.ts.map +1 -1
  126. package/dist/fields/TextField.js +2 -2
  127. package/dist/fields/TextField.js.map +1 -1
  128. package/dist/fields/ToggleField.d.ts.map +1 -1
  129. package/dist/fields/ToggleField.js +1 -1
  130. package/dist/fields/ToggleField.js.map +1 -1
  131. package/dist/fields/block-types.d.ts.map +1 -1
  132. package/dist/fields/block-types.js +28 -8
  133. package/dist/fields/block-types.js.map +1 -1
  134. package/dist/fields/index.d.ts.map +1 -1
  135. package/dist/fields/index.js.map +1 -1
  136. package/dist/hooks/useBuilderState.d.ts.map +1 -1
  137. package/dist/hooks/useBuilderState.js.map +1 -1
  138. package/dist/hooks/useContentLock.d.ts.map +1 -1
  139. package/dist/hooks/useContentLock.js.map +1 -1
  140. package/dist/hooks/useDebounce.js.map +1 -1
  141. package/dist/hooks/useKeyboardShortcuts.d.ts.map +1 -1
  142. package/dist/hooks/useKeyboardShortcuts.js.map +1 -1
  143. package/dist/index.d.ts +2 -2
  144. package/dist/index.d.ts.map +1 -1
  145. package/dist/index.js.map +1 -1
  146. package/dist/layout/Header.d.ts.map +1 -1
  147. package/dist/layout/Header.js.map +1 -1
  148. package/dist/layout/Layout.d.ts.map +1 -1
  149. package/dist/layout/Layout.js.map +1 -1
  150. package/dist/layout/Sidebar.d.ts +1 -1
  151. package/dist/layout/Sidebar.d.ts.map +1 -1
  152. package/dist/layout/Sidebar.js +5 -8
  153. package/dist/layout/Sidebar.js.map +1 -1
  154. package/dist/lib/api.js.map +1 -1
  155. package/dist/lib/search.d.ts.map +1 -1
  156. package/dist/lib/search.js +3 -5
  157. package/dist/lib/search.js.map +1 -1
  158. package/dist/lib/useApiData.d.ts.map +1 -1
  159. package/dist/lib/useApiData.js.map +1 -1
  160. package/dist/lib/utils.d.ts.map +1 -1
  161. package/dist/lib/utils.js.map +1 -1
  162. package/dist/router/index.d.ts.map +1 -1
  163. package/dist/router/index.js +1 -3
  164. package/dist/router/index.js.map +1 -1
  165. package/dist/views/CollectionList.d.ts.map +1 -1
  166. package/dist/views/CollectionList.js +56 -17
  167. package/dist/views/CollectionList.js.map +1 -1
  168. package/dist/views/Dashboard.d.ts.map +1 -1
  169. package/dist/views/Dashboard.js +26 -13
  170. package/dist/views/Dashboard.js.map +1 -1
  171. package/dist/views/DocumentEdit.d.ts +1 -1
  172. package/dist/views/DocumentEdit.d.ts.map +1 -1
  173. package/dist/views/DocumentEdit.js +33 -15
  174. package/dist/views/DocumentEdit.js.map +1 -1
  175. package/dist/views/ForgotPassword.d.ts.map +1 -1
  176. package/dist/views/ForgotPassword.js.map +1 -1
  177. package/dist/views/FormEditor.d.ts.map +1 -1
  178. package/dist/views/FormEditor.js +8 -2
  179. package/dist/views/FormEditor.js.map +1 -1
  180. package/dist/views/FormSubmissions.d.ts.map +1 -1
  181. package/dist/views/FormSubmissions.js +6 -6
  182. package/dist/views/FormSubmissions.js.map +1 -1
  183. package/dist/views/Forms.d.ts.map +1 -1
  184. package/dist/views/Forms.js.map +1 -1
  185. package/dist/views/Login.d.ts.map +1 -1
  186. package/dist/views/Login.js +5 -2
  187. package/dist/views/Login.js.map +1 -1
  188. package/dist/views/MediaBrowser.d.ts.map +1 -1
  189. package/dist/views/MediaBrowser.js +39 -19
  190. package/dist/views/MediaBrowser.js.map +1 -1
  191. package/dist/views/PageEditor.d.ts.map +1 -1
  192. package/dist/views/PageEditor.js.map +1 -1
  193. package/dist/views/Pages.d.ts.map +1 -1
  194. package/dist/views/Pages.js +20 -10
  195. package/dist/views/Pages.js.map +1 -1
  196. package/dist/views/PostEditor.d.ts.map +1 -1
  197. package/dist/views/PostEditor.js.map +1 -1
  198. package/dist/views/Posts.d.ts.map +1 -1
  199. package/dist/views/Posts.js +13 -7
  200. package/dist/views/Posts.js.map +1 -1
  201. package/dist/views/Redirects.d.ts.map +1 -1
  202. package/dist/views/Redirects.js +17 -5
  203. package/dist/views/Redirects.js.map +1 -1
  204. package/dist/views/ResetPassword.d.ts.map +1 -1
  205. package/dist/views/ResetPassword.js.map +1 -1
  206. package/dist/views/SEO.d.ts.map +1 -1
  207. package/dist/views/SEO.js +40 -17
  208. package/dist/views/SEO.js.map +1 -1
  209. package/dist/views/ScriptTagEditor.d.ts.map +1 -1
  210. package/dist/views/ScriptTagEditor.js +2 -1
  211. package/dist/views/ScriptTagEditor.js.map +1 -1
  212. package/dist/views/ScriptTags.d.ts.map +1 -1
  213. package/dist/views/ScriptTags.js.map +1 -1
  214. package/dist/views/Settings.d.ts.map +1 -1
  215. package/dist/views/Settings.js +38 -11
  216. package/dist/views/Settings.js.map +1 -1
  217. package/dist/views/SetupWizard.d.ts.map +1 -1
  218. package/dist/views/SetupWizard.js.map +1 -1
  219. package/dist/views/Users.d.ts.map +1 -1
  220. package/dist/views/Users.js +5 -3
  221. package/dist/views/Users.js.map +1 -1
  222. package/dist/views/page-builder/AIBlockAssist.d.ts.map +1 -1
  223. package/dist/views/page-builder/AIBlockAssist.js +1 -1
  224. package/dist/views/page-builder/AIBlockAssist.js.map +1 -1
  225. package/dist/views/page-builder/AIGenerateDialog.d.ts.map +1 -1
  226. package/dist/views/page-builder/AIGenerateDialog.js +4 -1
  227. package/dist/views/page-builder/AIGenerateDialog.js.map +1 -1
  228. package/dist/views/page-builder/BlockEditor.d.ts.map +1 -1
  229. package/dist/views/page-builder/BlockEditor.js +1 -1
  230. package/dist/views/page-builder/BlockEditor.js.map +1 -1
  231. package/dist/views/page-builder/BlockPicker.d.ts.map +1 -1
  232. package/dist/views/page-builder/BlockPicker.js.map +1 -1
  233. package/dist/views/page-builder/BottomBar.d.ts.map +1 -1
  234. package/dist/views/page-builder/BottomBar.js.map +1 -1
  235. package/dist/views/page-builder/BuilderToolbar.d.ts.map +1 -1
  236. package/dist/views/page-builder/BuilderToolbar.js.map +1 -1
  237. package/dist/views/page-builder/ContextPanel.d.ts.map +1 -1
  238. package/dist/views/page-builder/ContextPanel.js +4 -1
  239. package/dist/views/page-builder/ContextPanel.js.map +1 -1
  240. package/dist/views/page-builder/DesignScore.d.ts.map +1 -1
  241. package/dist/views/page-builder/DesignScore.js.map +1 -1
  242. package/dist/views/page-builder/NodeSettings.d.ts.map +1 -1
  243. package/dist/views/page-builder/NodeSettings.js +1 -1
  244. package/dist/views/page-builder/NodeSettings.js.map +1 -1
  245. package/dist/views/page-builder/PageBuilder.d.ts +1 -1
  246. package/dist/views/page-builder/PageBuilder.d.ts.map +1 -1
  247. package/dist/views/page-builder/PageBuilder.js +4 -2
  248. package/dist/views/page-builder/PageBuilder.js.map +1 -1
  249. package/dist/views/page-builder/PageSettings.d.ts.map +1 -1
  250. package/dist/views/page-builder/PageSettings.js.map +1 -1
  251. package/dist/views/page-builder/PageTemplates.d.ts.map +1 -1
  252. package/dist/views/page-builder/PageTemplates.js.map +1 -1
  253. package/dist/views/page-builder/SEOPanel.d.ts.map +1 -1
  254. package/dist/views/page-builder/SEOPanel.js +1 -3
  255. package/dist/views/page-builder/SEOPanel.js.map +1 -1
  256. package/dist/views/page-builder/SavedSections.d.ts.map +1 -1
  257. package/dist/views/page-builder/SavedSections.js +3 -7
  258. package/dist/views/page-builder/SavedSections.js.map +1 -1
  259. package/dist/views/page-builder/TemplatePicker.d.ts.map +1 -1
  260. package/dist/views/page-builder/TemplatePicker.js.map +1 -1
  261. package/dist/views/page-builder/block-renderers/CTAPreview.d.ts.map +1 -1
  262. package/dist/views/page-builder/block-renderers/CTAPreview.js +1 -1
  263. package/dist/views/page-builder/block-renderers/CTAPreview.js.map +1 -1
  264. package/dist/views/page-builder/block-renderers/CardsPreview.d.ts.map +1 -1
  265. package/dist/views/page-builder/block-renderers/CardsPreview.js +1 -1
  266. package/dist/views/page-builder/block-renderers/CardsPreview.js.map +1 -1
  267. package/dist/views/page-builder/block-renderers/CodePreview.d.ts.map +1 -1
  268. package/dist/views/page-builder/block-renderers/CodePreview.js +1 -5
  269. package/dist/views/page-builder/block-renderers/CodePreview.js.map +1 -1
  270. package/dist/views/page-builder/block-renderers/FAQPreview.d.ts.map +1 -1
  271. package/dist/views/page-builder/block-renderers/FAQPreview.js +4 -1
  272. package/dist/views/page-builder/block-renderers/FAQPreview.js.map +1 -1
  273. package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts.map +1 -1
  274. package/dist/views/page-builder/block-renderers/FallbackPreview.js.map +1 -1
  275. package/dist/views/page-builder/block-renderers/FormPreview.d.ts.map +1 -1
  276. package/dist/views/page-builder/block-renderers/FormPreview.js +2 -2
  277. package/dist/views/page-builder/block-renderers/FormPreview.js.map +1 -1
  278. package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts.map +1 -1
  279. package/dist/views/page-builder/block-renderers/GalleryPreview.js +1 -3
  280. package/dist/views/page-builder/block-renderers/GalleryPreview.js.map +1 -1
  281. package/dist/views/page-builder/block-renderers/HeroPreview.d.ts.map +1 -1
  282. package/dist/views/page-builder/block-renderers/HeroPreview.js.map +1 -1
  283. package/dist/views/page-builder/block-renderers/ImagePreview.d.ts.map +1 -1
  284. package/dist/views/page-builder/block-renderers/ImagePreview.js.map +1 -1
  285. package/dist/views/page-builder/block-renderers/TextPreview.d.ts.map +1 -1
  286. package/dist/views/page-builder/block-renderers/TextPreview.js +2 -6
  287. package/dist/views/page-builder/block-renderers/TextPreview.js.map +1 -1
  288. package/dist/views/page-builder/block-renderers/VideoPreview.d.ts.map +1 -1
  289. package/dist/views/page-builder/block-renderers/VideoPreview.js +2 -5
  290. package/dist/views/page-builder/block-renderers/VideoPreview.js.map +1 -1
  291. package/dist/views/page-builder/block-renderers/index.d.ts.map +1 -1
  292. package/dist/views/page-builder/block-renderers/index.js.map +1 -1
  293. package/dist/views/page-builder/canvas/BlockRenderer.d.ts.map +1 -1
  294. package/dist/views/page-builder/canvas/BlockRenderer.js +1 -5
  295. package/dist/views/page-builder/canvas/BlockRenderer.js.map +1 -1
  296. package/dist/views/page-builder/canvas/BuilderCanvas.d.ts.map +1 -1
  297. package/dist/views/page-builder/canvas/BuilderCanvas.js.map +1 -1
  298. package/dist/views/page-builder/canvas/ColumnRenderer.d.ts.map +1 -1
  299. package/dist/views/page-builder/canvas/ColumnRenderer.js +1 -5
  300. package/dist/views/page-builder/canvas/ColumnRenderer.js.map +1 -1
  301. package/dist/views/page-builder/canvas/ContainerRenderer.d.ts.map +1 -1
  302. package/dist/views/page-builder/canvas/ContainerRenderer.js +1 -5
  303. package/dist/views/page-builder/canvas/ContainerRenderer.js.map +1 -1
  304. package/dist/views/page-builder/canvas/RowRenderer.d.ts.map +1 -1
  305. package/dist/views/page-builder/canvas/RowRenderer.js +1 -5
  306. package/dist/views/page-builder/canvas/RowRenderer.js.map +1 -1
  307. package/dist/views/page-builder/canvas/SectionRenderer.d.ts.map +1 -1
  308. package/dist/views/page-builder/canvas/SectionRenderer.js +1 -5
  309. package/dist/views/page-builder/canvas/SectionRenderer.js.map +1 -1
  310. package/dist/views/page-builder/canvas/index.d.ts.map +1 -1
  311. package/dist/views/page-builder/canvas/index.js.map +1 -1
  312. package/package.json +2 -2
  313. package/src/AdminRoot.tsx +263 -191
  314. package/src/__tests__/lib/search.test.ts +60 -69
  315. package/src/__tests__/lib/utils.test.ts +12 -12
  316. package/src/__tests__/router/match-route.test.ts +24 -26
  317. package/src/__tests__/router/strip-base.test.ts +15 -15
  318. package/src/components/Breadcrumbs.tsx +27 -24
  319. package/src/components/CommandPalette.tsx +115 -99
  320. package/src/components/ContentOverviewChart.tsx +19 -14
  321. package/src/components/ErrorBoundary.tsx +13 -13
  322. package/src/components/FocalPointPicker.tsx +31 -20
  323. package/src/components/FolderTree.tsx +172 -139
  324. package/src/components/LivePreview.tsx +68 -41
  325. package/src/components/LocaleProvider.tsx +26 -20
  326. package/src/components/LocaleSwitcher.tsx +9 -11
  327. package/src/components/MediaPickerModal.tsx +46 -45
  328. package/src/components/PresenceIndicator.tsx +30 -27
  329. package/src/components/SEOPanel.tsx +378 -228
  330. package/src/components/SEOPerformance.tsx +52 -30
  331. package/src/components/ThemeProvider.tsx +46 -46
  332. package/src/components/TipTapEditor.tsx +60 -64
  333. package/src/components/VersionHistory.tsx +63 -52
  334. package/src/components/ui/Avatar.tsx +8 -8
  335. package/src/components/ui/Badge.tsx +7 -5
  336. package/src/components/ui/Button.tsx +24 -13
  337. package/src/components/ui/CommandPalette.tsx +56 -42
  338. package/src/components/ui/ConfirmDialog.tsx +14 -14
  339. package/src/components/ui/DataTable.tsx +37 -39
  340. package/src/components/ui/EmptyState.tsx +9 -11
  341. package/src/components/ui/Modal.tsx +21 -15
  342. package/src/components/ui/Pagination.tsx +34 -19
  343. package/src/components/ui/SearchInput.tsx +17 -7
  344. package/src/components/ui/Skeleton.tsx +7 -7
  345. package/src/components/ui/Toast.tsx +29 -22
  346. package/src/components/ui/index.ts +24 -24
  347. package/src/fields/ArrayField.tsx +43 -25
  348. package/src/fields/BlockBuilderField.tsx +80 -99
  349. package/src/fields/DateField.tsx +20 -12
  350. package/src/fields/FieldRenderer.tsx +34 -34
  351. package/src/fields/GroupField.tsx +8 -10
  352. package/src/fields/MediaField.tsx +8 -10
  353. package/src/fields/NavBuilderField.tsx +24 -25
  354. package/src/fields/NumberField.tsx +21 -14
  355. package/src/fields/RelationshipField.tsx +105 -91
  356. package/src/fields/RichTextField.tsx +16 -12
  357. package/src/fields/SelectField.tsx +42 -34
  358. package/src/fields/SlugField.tsx +29 -17
  359. package/src/fields/TextField.tsx +24 -16
  360. package/src/fields/ToggleField.tsx +7 -9
  361. package/src/fields/block-types.ts +50 -24
  362. package/src/fields/index.ts +17 -17
  363. package/src/hooks/useBuilderState.ts +260 -221
  364. package/src/hooks/useContentLock.ts +23 -20
  365. package/src/hooks/useDebounce.ts +7 -7
  366. package/src/hooks/useKeyboardShortcuts.ts +16 -16
  367. package/src/index.ts +69 -58
  368. package/src/layout/Header.tsx +21 -20
  369. package/src/layout/Layout.tsx +22 -24
  370. package/src/layout/Sidebar.tsx +107 -72
  371. package/src/lib/api.ts +34 -34
  372. package/src/lib/search.ts +30 -34
  373. package/src/lib/useApiData.ts +65 -62
  374. package/src/lib/utils.ts +3 -3
  375. package/src/router/index.ts +33 -35
  376. package/src/styles/build-input.css +2 -2
  377. package/src/styles/tailwind.css +1 -1
  378. package/src/styles/theme.css +26 -2
  379. package/src/views/CollectionList.tsx +275 -121
  380. package/src/views/Dashboard.tsx +164 -117
  381. package/src/views/DocumentEdit.tsx +298 -253
  382. package/src/views/ForgotPassword.tsx +27 -23
  383. package/src/views/FormEditor.tsx +165 -99
  384. package/src/views/FormSubmissions.tsx +261 -117
  385. package/src/views/Forms.tsx +56 -26
  386. package/src/views/Login.tsx +107 -84
  387. package/src/views/MediaBrowser.tsx +717 -523
  388. package/src/views/PageEditor.tsx +44 -46
  389. package/src/views/Pages.tsx +312 -149
  390. package/src/views/PostEditor.tsx +57 -51
  391. package/src/views/Posts.tsx +206 -74
  392. package/src/views/Redirects.tsx +173 -117
  393. package/src/views/ResetPassword.tsx +43 -32
  394. package/src/views/SEO.tsx +607 -160
  395. package/src/views/ScriptTagEditor.tsx +69 -69
  396. package/src/views/ScriptTags.tsx +54 -42
  397. package/src/views/Settings.tsx +430 -220
  398. package/src/views/SetupWizard.tsx +69 -46
  399. package/src/views/Users.tsx +154 -120
  400. package/src/views/page-builder/AIBlockAssist.tsx +21 -25
  401. package/src/views/page-builder/AIGenerateDialog.tsx +134 -127
  402. package/src/views/page-builder/BlockEditor.tsx +94 -96
  403. package/src/views/page-builder/BlockPicker.tsx +73 -88
  404. package/src/views/page-builder/BottomBar.tsx +15 -11
  405. package/src/views/page-builder/BuilderToolbar.tsx +32 -29
  406. package/src/views/page-builder/ContextPanel.tsx +57 -57
  407. package/src/views/page-builder/DesignScore.tsx +52 -59
  408. package/src/views/page-builder/NodeSettings.tsx +59 -59
  409. package/src/views/page-builder/PageBuilder.tsx +156 -155
  410. package/src/views/page-builder/PageSettings.tsx +16 -15
  411. package/src/views/page-builder/PageTemplates.tsx +23 -17
  412. package/src/views/page-builder/SEOPanel.tsx +90 -111
  413. package/src/views/page-builder/SavedSections.tsx +99 -105
  414. package/src/views/page-builder/TemplatePicker.tsx +44 -48
  415. package/src/views/page-builder/block-renderers/CTAPreview.tsx +11 -13
  416. package/src/views/page-builder/block-renderers/CardsPreview.tsx +13 -15
  417. package/src/views/page-builder/block-renderers/CodePreview.tsx +16 -16
  418. package/src/views/page-builder/block-renderers/FAQPreview.tsx +20 -23
  419. package/src/views/page-builder/block-renderers/FallbackPreview.tsx +5 -5
  420. package/src/views/page-builder/block-renderers/FormPreview.tsx +9 -13
  421. package/src/views/page-builder/block-renderers/GalleryPreview.tsx +22 -28
  422. package/src/views/page-builder/block-renderers/HeroPreview.tsx +17 -30
  423. package/src/views/page-builder/block-renderers/ImagePreview.tsx +12 -12
  424. package/src/views/page-builder/block-renderers/TextPreview.tsx +22 -22
  425. package/src/views/page-builder/block-renderers/VideoPreview.tsx +13 -18
  426. package/src/views/page-builder/block-renderers/index.ts +17 -17
  427. package/src/views/page-builder/canvas/BlockRenderer.tsx +19 -23
  428. package/src/views/page-builder/canvas/BuilderCanvas.tsx +17 -20
  429. package/src/views/page-builder/canvas/ColumnRenderer.tsx +22 -26
  430. package/src/views/page-builder/canvas/ContainerRenderer.tsx +20 -24
  431. package/src/views/page-builder/canvas/RowRenderer.tsx +19 -23
  432. package/src/views/page-builder/canvas/SectionRenderer.tsx +30 -34
  433. package/src/views/page-builder/canvas/index.ts +2 -2
@@ -1,44 +1,66 @@
1
- 'use client';
1
+ 'use client'
2
2
 
3
3
  import {
4
- Upload, Grid3x3, List, Search, Trash2, Download, FileText,
5
- ArrowUpDown, ArrowUp, ArrowDown, X, Bot, Sparkles, Link2,
6
- AlertTriangle, Copy, ExternalLink, ImageIcon, FileImage, Loader2,
7
- FolderInput, GripVertical,
8
- } from 'lucide-react';
9
- import { useState, useMemo, useRef, useCallback } from 'react';
10
- import { toast } from 'sonner';
11
- import { sortByRelevance, type SortConfig, toggleSort } from '../lib/search.js';
12
- import { useApiData } from '../lib/useApiData.js';
13
- import { cmsApi } from '../lib/api.js';
14
- import { FolderTree, type FolderSelection } from '../components/FolderTree.js';
15
- import { FocalPointPicker } from '../components/FocalPointPicker.js';
4
+ Upload,
5
+ Grid3x3,
6
+ List,
7
+ Search,
8
+ Trash2,
9
+ Download,
10
+ FileText,
11
+ ArrowUpDown,
12
+ ArrowUp,
13
+ ArrowDown,
14
+ X,
15
+ Bot,
16
+ Sparkles,
17
+ Link2,
18
+ AlertTriangle,
19
+ Copy,
20
+ ExternalLink,
21
+ ImageIcon,
22
+ FileImage,
23
+ Loader2,
24
+ FolderInput,
25
+ GripVertical,
26
+ } from 'lucide-react'
27
+ import { useState, useMemo, useRef, useCallback } from 'react'
28
+ import { toast } from 'sonner'
29
+ import { sortByRelevance, type SortConfig, toggleSort } from '../lib/search.js'
30
+ import { useApiData } from '../lib/useApiData.js'
31
+ import { cmsApi } from '../lib/api.js'
32
+ import { FolderTree, type FolderSelection } from '../components/FolderTree.js'
33
+ import { FocalPointPicker } from '../components/FocalPointPicker.js'
16
34
 
17
35
  interface MediaItem {
18
- id: number | string;
19
- name: string;
20
- type: string;
21
- size: string;
22
- sizeBytes: number;
23
- date: string;
24
- url: string;
25
- dimensions?: string;
26
- format?: string;
27
- altTag?: string;
28
- title?: string;
29
- usedOn?: { page: string; path: string }[];
36
+ id: number | string
37
+ name: string
38
+ type: string
39
+ size: string
40
+ sizeBytes: number
41
+ date: string
42
+ url: string
43
+ dimensions?: string
44
+ format?: string
45
+ altTag?: string
46
+ title?: string
47
+ usedOn?: { page: string; path: string }[]
30
48
  }
31
49
 
32
50
  function isImageMedia(item: MediaItem): boolean {
33
- return Boolean(item.url) && (item.type?.startsWith('image/') || /\.(avif|gif|jpe?g|png|webp|svg)$/i.test(item.url));
51
+ return (
52
+ Boolean(item.url) &&
53
+ (item.type?.startsWith('image/') || /\.(avif|gif|jpe?g|png|webp|svg)$/i.test(item.url))
54
+ )
34
55
  }
35
56
 
36
57
  function matchesMediaType(item: MediaItem, filterType: string): boolean {
37
- if (filterType === 'all') return true;
38
- if (filterType === 'image') return item.type?.startsWith('image/') || isImageMedia(item);
39
- if (filterType === 'video') return item.type?.startsWith('video/');
40
- if (filterType === 'document') return !item.type?.startsWith('image/') && !item.type?.startsWith('video/');
41
- return item.type === filterType;
58
+ if (filterType === 'all') return true
59
+ if (filterType === 'image') return item.type?.startsWith('image/') || isImageMedia(item)
60
+ if (filterType === 'video') return item.type?.startsWith('video/')
61
+ if (filterType === 'document')
62
+ return !item.type?.startsWith('image/') && !item.type?.startsWith('video/')
63
+ return item.type === filterType
42
64
  }
43
65
 
44
66
  function MediaPreview({ item }: { item: MediaItem }) {
@@ -50,105 +72,115 @@ function MediaPreview({ item }: { item: MediaItem }) {
50
72
  className="h-full w-full object-cover"
51
73
  loading="lazy"
52
74
  />
53
- );
75
+ )
54
76
  }
55
77
 
56
- return <FileImage className="w-6 h-6 sm:w-8 sm:h-8 text-muted-foreground" />;
78
+ return <FileImage className="w-6 h-6 sm:w-8 sm:h-8 text-muted-foreground" />
57
79
  }
58
80
 
59
- type MediaSortKey = 'name' | 'type' | 'size' | 'date';
81
+ type MediaSortKey = 'name' | 'type' | 'size' | 'date'
60
82
 
61
83
  export interface MediaBrowserProps {
62
- onNavigate?: (path: string) => void;
84
+ onNavigate?: (path: string) => void
63
85
  }
64
86
 
65
87
  function buildMediaApiUrl(folderSel: FolderSelection): string {
66
- const base = '/media?pageSize=100';
88
+ const base = '/media?pageSize=100'
67
89
  if (folderSel.type === 'smart') {
68
- if (folderSel.smart === 'recent') return `${base}&sort=updatedAt&order=desc&pageSize=20`;
69
- if (folderSel.smart === 'uncategorized') return `${base}&folderId=none`;
70
- return base;
90
+ if (folderSel.smart === 'recent') return `${base}&sort=updatedAt&order=desc&pageSize=20`
91
+ if (folderSel.smart === 'uncategorized') return `${base}&folderId=none`
92
+ return base
71
93
  }
72
- return `${base}&folderId=${folderSel.folderId}`;
94
+ return `${base}&folderId=${folderSel.folderId}`
73
95
  }
74
96
 
75
97
  export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
76
- const [folderSel, setFolderSel] = useState<FolderSelection>({ type: 'smart', smart: 'all' });
77
- const [sidebarOpen, setSidebarOpen] = useState(true);
78
-
79
- const apiUrl = useMemo(() => buildMediaApiUrl(folderSel), [folderSel]);
80
- const { data, loading, error, refetch } = useApiData<{ data?: MediaItem[]; items?: MediaItem[]; total: number }>(apiUrl);
81
-
82
- const allData = useApiData<{ data: MediaItem[]; total: number }>('/media?pageSize=1');
83
- const uncatData = useApiData<{ data: MediaItem[]; total: number }>('/media?pageSize=1&folderId=none');
84
-
85
- const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
86
- const [searchQuery, setSearchQuery] = useState('');
87
- const [filterType, setFilterType] = useState('all');
88
- const [selectedMedia, setSelectedMedia] = useState<Array<number | string>>([]);
89
- const [sortConfig, setSortConfig] = useState<SortConfig<MediaSortKey> | null>(null);
90
- const [activeItem, setActiveItem] = useState<MediaItem | null>(null);
91
-
92
- const [editAlt, setEditAlt] = useState('');
93
- const [editTitle, setEditTitle] = useState('');
94
- const [editFilename, setEditFilename] = useState('');
95
- const [focalX, setFocalX] = useState(0.5);
96
- const [focalY, setFocalY] = useState(0.5);
97
- const [saving, setSaving] = useState(false);
98
- const [aiGenerating, setAiGenerating] = useState<string | null>(null);
99
- const [uploading, setUploading] = useState(false);
100
- const fileInputRef = useRef<HTMLInputElement>(null);
101
-
102
- const mediaItems = data?.data ?? data?.items ?? [];
98
+ const [folderSel, setFolderSel] = useState<FolderSelection>({ type: 'smart', smart: 'all' })
99
+ const [sidebarOpen, setSidebarOpen] = useState(true)
100
+
101
+ const apiUrl = useMemo(() => buildMediaApiUrl(folderSel), [folderSel])
102
+ const { data, loading, error, refetch } = useApiData<{
103
+ data?: MediaItem[]
104
+ items?: MediaItem[]
105
+ total: number
106
+ }>(apiUrl)
107
+
108
+ const allData = useApiData<{ data: MediaItem[]; total: number }>('/media?pageSize=1')
109
+ const uncatData = useApiData<{ data: MediaItem[]; total: number }>(
110
+ '/media?pageSize=1&folderId=none',
111
+ )
112
+
113
+ const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
114
+ const [searchQuery, setSearchQuery] = useState('')
115
+ const [filterType, setFilterType] = useState('all')
116
+ const [selectedMedia, setSelectedMedia] = useState<Array<number | string>>([])
117
+ const [sortConfig, setSortConfig] = useState<SortConfig<MediaSortKey> | null>(null)
118
+ const [activeItem, setActiveItem] = useState<MediaItem | null>(null)
119
+
120
+ const [editAlt, setEditAlt] = useState('')
121
+ const [editTitle, setEditTitle] = useState('')
122
+ const [editFilename, setEditFilename] = useState('')
123
+ const [focalX, setFocalX] = useState(0.5)
124
+ const [focalY, setFocalY] = useState(0.5)
125
+ const [saving, setSaving] = useState(false)
126
+ const [aiGenerating, setAiGenerating] = useState<string | null>(null)
127
+ const [uploading, setUploading] = useState(false)
128
+ const fileInputRef = useRef<HTMLInputElement>(null)
129
+
130
+ const mediaItems = data?.data ?? data?.items ?? []
103
131
 
104
132
  const filteredAndSorted = useMemo(() => {
105
133
  let results = mediaItems.filter((item) => {
106
- const matchesSearch = (item.name ?? '').toLowerCase().includes(searchQuery.toLowerCase());
107
- const matchesType = matchesMediaType(item, filterType);
108
- return matchesSearch && matchesType;
109
- });
134
+ const matchesSearch = (item.name ?? '').toLowerCase().includes(searchQuery.toLowerCase())
135
+ const matchesType = matchesMediaType(item, filterType)
136
+ return matchesSearch && matchesType
137
+ })
110
138
 
111
139
  if (searchQuery.trim()) {
112
- results = sortByRelevance(results, searchQuery, (m) => [m.name]);
140
+ results = sortByRelevance(results, searchQuery, (m) => [m.name])
113
141
  } else if (sortConfig) {
114
142
  results = [...results].sort((a, b) => {
115
- let cmp: number;
143
+ let cmp: number
116
144
  if (sortConfig.key === 'size') {
117
- cmp = a.sizeBytes - b.sizeBytes;
145
+ cmp = a.sizeBytes - b.sizeBytes
118
146
  } else {
119
- cmp = String(a[sortConfig.key]).localeCompare(String(b[sortConfig.key]));
147
+ cmp = String(a[sortConfig.key]).localeCompare(String(b[sortConfig.key]))
120
148
  }
121
- return sortConfig.direction === 'asc' ? cmp : -cmp;
122
- });
149
+ return sortConfig.direction === 'asc' ? cmp : -cmp
150
+ })
123
151
  }
124
- return results;
125
- }, [mediaItems, searchQuery, filterType, sortConfig]);
152
+ return results
153
+ }, [mediaItems, searchQuery, filterType, sortConfig])
126
154
 
127
155
  const openDetail = (item: MediaItem) => {
128
- setActiveItem(item);
129
- setEditAlt(item.altTag ?? '');
130
- setEditTitle(item.title ?? '');
131
- setEditFilename(item.name);
132
- setFocalX((item as any).focalPointX ?? 0.5);
133
- setFocalY((item as any).focalPointY ?? 0.5);
134
- };
156
+ setActiveItem(item)
157
+ setEditAlt(item.altTag ?? '')
158
+ setEditTitle(item.title ?? '')
159
+ setEditFilename(item.name)
160
+ setFocalX((item as any).focalPointX ?? 0.5)
161
+ setFocalY((item as any).focalPointY ?? 0.5)
162
+ }
135
163
 
136
164
  const closeDetail = () => {
137
- setActiveItem(null);
138
- };
165
+ setActiveItem(null)
166
+ }
139
167
 
140
168
  const handleCheckbox = (e: React.MouseEvent, id: number | string) => {
141
- e.stopPropagation();
142
- setSelectedMedia(prev => prev.includes(id) ? prev.filter(item => item !== id) : [...prev, id]);
143
- };
169
+ e.stopPropagation()
170
+ setSelectedMedia((prev) =>
171
+ prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id],
172
+ )
173
+ }
144
174
 
145
175
  const handleSelectAll = () => {
146
- setSelectedMedia(prev => prev.length === filteredAndSorted.length ? [] : filteredAndSorted.map(item => item.id));
147
- };
176
+ setSelectedMedia((prev) =>
177
+ prev.length === filteredAndSorted.length ? [] : filteredAndSorted.map((item) => item.id),
178
+ )
179
+ }
148
180
 
149
181
  const handleSaveDetails = async () => {
150
- if (!activeItem) return;
151
- setSaving(true);
182
+ if (!activeItem) return
183
+ setSaving(true)
152
184
  const res = await cmsApi(`/media/${activeItem.id}`, {
153
185
  method: 'PUT',
154
186
  body: JSON.stringify({
@@ -158,147 +190,160 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
158
190
  focalX,
159
191
  focalY,
160
192
  }),
161
- });
162
- setSaving(false);
193
+ })
194
+ setSaving(false)
163
195
  if (res.error) {
164
- toast.error(res.error);
196
+ toast.error(res.error)
165
197
  } else {
166
- toast.success('Media details saved');
167
- refetch();
198
+ toast.success('Media details saved')
199
+ refetch()
168
200
  }
169
- };
201
+ }
170
202
 
171
203
  const deleteMedia = async (id: number | string) => {
172
- const res = await cmsApi(`/media/${id}`, { method: 'DELETE' });
204
+ const res = await cmsApi(`/media/${id}`, { method: 'DELETE' })
173
205
  if (res.error) {
174
- toast.error(res.error);
206
+ toast.error(res.error)
175
207
  } else {
176
- toast.success('Media deleted');
177
- if (activeItem?.id === id) closeDetail();
178
- refetch();
208
+ toast.success('Media deleted')
209
+ if (activeItem?.id === id) closeDetail()
210
+ refetch()
179
211
  }
180
- };
212
+ }
181
213
 
182
214
  const handleAiGenerate = async (field: 'alt' | 'title' | 'optimize') => {
183
- setAiGenerating(field);
215
+ setAiGenerating(field)
184
216
 
185
217
  if (field === 'optimize' && activeItem) {
186
- const res = await cmsApi<MediaItem & {
187
- optimization?: {
188
- originalSize: number;
189
- optimizedSize: number;
190
- savings: number;
191
- originalSizeFormatted: string;
192
- optimizedSizeFormatted: string;
193
- alreadyOptimized?: boolean;
194
- };
195
- }>(`/media/${activeItem.id}/optimize`, { method: 'POST' });
218
+ const res = await cmsApi<
219
+ MediaItem & {
220
+ optimization?: {
221
+ originalSize: number
222
+ optimizedSize: number
223
+ savings: number
224
+ originalSizeFormatted: string
225
+ optimizedSizeFormatted: string
226
+ alreadyOptimized?: boolean
227
+ }
228
+ }
229
+ >(`/media/${activeItem.id}/optimize`, { method: 'POST' })
196
230
 
197
231
  if (res.error) {
198
- toast.error(res.error);
232
+ toast.error(res.error)
199
233
  } else if ((res.data as any)?.optimization?.alreadyOptimized) {
200
- toast.info('Image is already in WebP format — no further optimization needed');
234
+ toast.info('Image is already in WebP format — no further optimization needed')
201
235
  } else if ((res.data as any)?.optimization) {
202
- const opt = (res.data as any).optimization;
236
+ const opt = (res.data as any).optimization
203
237
  toast.success(
204
238
  `Optimized: ${opt.originalSizeFormatted} → ${opt.optimizedSizeFormatted} (${opt.savings}% smaller)`,
205
- );
206
- refetch();
239
+ )
240
+ refetch()
207
241
  }
208
242
 
209
- setAiGenerating(null);
210
- return;
243
+ setAiGenerating(null)
244
+ return
211
245
  }
212
246
 
213
- await new Promise(r => setTimeout(r, 1500));
247
+ await new Promise((r) => setTimeout(r, 1500))
214
248
  if (field === 'alt') {
215
- const generated = `A ${(activeItem?.format ?? 'media').toLowerCase()} image showing ${(activeItem?.name ?? 'uploaded').replace(/[-_]/g, ' ').replace(/\.\w+$/, '')} content`;
216
- setEditAlt(generated);
217
- toast.success('Alt tag generated by AI');
249
+ const generated = `A ${(activeItem?.format ?? 'media').toLowerCase()} image showing ${(activeItem?.name ?? 'uploaded').replace(/[-_]/g, ' ').replace(/\.\w+$/, '')} content`
250
+ setEditAlt(generated)
251
+ toast.success('Alt tag generated by AI')
218
252
  } else if (field === 'title') {
219
- const generated = activeItem?.name.replace(/[-_]/g, ' ').replace(/\.\w+$/, '').replace(/\b\w/g, c => c.toUpperCase()) ?? '';
220
- setEditTitle(generated);
221
- toast.success('Title generated by AI');
253
+ const generated =
254
+ activeItem?.name
255
+ .replace(/[-_]/g, ' ')
256
+ .replace(/\.\w+$/, '')
257
+ .replace(/\b\w/g, (c) => c.toUpperCase()) ?? ''
258
+ setEditTitle(generated)
259
+ toast.success('Title generated by AI')
222
260
  }
223
- setAiGenerating(null);
224
- };
261
+ setAiGenerating(null)
262
+ }
225
263
 
226
264
  const handleCopyUrl = () => {
227
265
  if (activeItem?.url) {
228
- navigator.clipboard.writeText(activeItem.url);
229
- toast.success('URL copied to clipboard');
266
+ navigator.clipboard.writeText(activeItem.url)
267
+ toast.success('URL copied to clipboard')
230
268
  }
231
- };
269
+ }
232
270
 
233
271
  const handleUploadFiles = async (files: FileList | null) => {
234
- if (!files || files.length === 0) return;
235
- setUploading(true);
272
+ if (!files || files.length === 0) return
273
+ setUploading(true)
236
274
 
237
- let successCount = 0;
275
+ let successCount = 0
238
276
 
239
277
  for (let i = 0; i < files.length; i++) {
240
- const file = files[i]!;
241
- const formData = new FormData();
242
- formData.append('file', file);
243
-
244
- const res = await cmsApi<MediaItem & {
245
- optimization?: {
246
- originalSize: number;
247
- optimizedSize: number;
248
- savings: number;
249
- originalSizeFormatted: string;
250
- optimizedSizeFormatted: string;
251
- };
252
- }>('/media/upload', { method: 'POST', body: formData });
278
+ const file = files[i]!
279
+ const formData = new FormData()
280
+ formData.append('file', file)
281
+
282
+ const res = await cmsApi<
283
+ MediaItem & {
284
+ optimization?: {
285
+ originalSize: number
286
+ optimizedSize: number
287
+ savings: number
288
+ originalSizeFormatted: string
289
+ optimizedSizeFormatted: string
290
+ }
291
+ }
292
+ >('/media/upload', { method: 'POST', body: formData })
253
293
 
254
294
  if (res.error) {
255
- toast.error(`Failed to upload ${file.name}: ${res.error}`);
295
+ toast.error(`Failed to upload ${file.name}: ${res.error}`)
256
296
  } else {
257
- const opt = (res.data as any)?.optimization;
297
+ const opt = (res.data as any)?.optimization
258
298
  if (opt && opt.savings > 0) {
259
299
  toast.success(
260
300
  `${file.name} → WebP (${opt.originalSizeFormatted} → ${opt.optimizedSizeFormatted}, ${opt.savings}% saved)`,
261
- );
301
+ )
262
302
  } else {
263
- toast.success(`Uploaded ${file.name}`);
303
+ toast.success(`Uploaded ${file.name}`)
264
304
  }
265
- successCount++;
305
+ successCount++
266
306
  }
267
307
  }
268
308
 
269
- if (successCount > 0) refetch();
270
- if (fileInputRef.current) fileInputRef.current.value = '';
271
- setUploading(false);
272
- };
309
+ if (successCount > 0) refetch()
310
+ if (fileInputRef.current) fileInputRef.current.value = ''
311
+ setUploading(false)
312
+ }
273
313
 
274
- const handleDropItem = useCallback(async (itemId: string, folderId: string | null) => {
275
- const res = await cmsApi(`/media/${itemId}/folder`, {
276
- method: 'PUT',
277
- body: JSON.stringify({ folderId }),
278
- });
279
- if (res.error) {
280
- toast.error(res.error);
281
- } else {
282
- toast.success(folderId ? 'Moved to folder' : 'Removed from folder');
283
- refetch();
284
- }
285
- }, [refetch]);
314
+ const handleDropItem = useCallback(
315
+ async (itemId: string, folderId: string | null) => {
316
+ const res = await cmsApi(`/media/${itemId}/folder`, {
317
+ method: 'PUT',
318
+ body: JSON.stringify({ folderId }),
319
+ })
320
+ if (res.error) {
321
+ toast.error(res.error)
322
+ } else {
323
+ toast.success(folderId ? 'Moved to folder' : 'Removed from folder')
324
+ refetch()
325
+ }
326
+ },
327
+ [refetch],
328
+ )
286
329
 
287
330
  const handleDragStart = (e: React.DragEvent, id: number | string) => {
288
- e.dataTransfer.setData('text/actuate-item-id', String(id));
289
- e.dataTransfer.effectAllowed = 'move';
290
- };
291
-
292
- const panelOpen = activeItem !== null;
293
- const issues = activeItem ? [
294
- ...(!activeItem.altTag ? ['Missing alt tag'] : []),
295
- ...(!activeItem.title ? ['Missing title'] : []),
296
- ...(activeItem.sizeBytes > 2000000 ? ['File size over 2 MB — consider optimizing'] : []),
297
- ...(activeItem.usedOn?.length === 0 ? ['Not used on any page'] : []),
298
- ] : [];
331
+ e.dataTransfer.setData('text/actuate-item-id', String(id))
332
+ e.dataTransfer.effectAllowed = 'move'
333
+ }
334
+
335
+ const panelOpen = activeItem !== null
336
+ const issues = activeItem
337
+ ? [
338
+ ...(!activeItem.altTag ? ['Missing alt tag'] : []),
339
+ ...(!activeItem.title ? ['Missing title'] : []),
340
+ ...(activeItem.sizeBytes > 2000000 ? ['File size over 2 MB — consider optimizing'] : []),
341
+ ...(activeItem.usedOn?.length === 0 ? ['Not used on any page'] : []),
342
+ ]
343
+ : []
299
344
 
300
345
  function SortHeader({ label, sortKey }: { label: string; sortKey: MediaSortKey }) {
301
- const active = sortConfig?.key === sortKey;
346
+ const active = sortConfig?.key === sortKey
302
347
  return (
303
348
  <button
304
349
  type="button"
@@ -307,12 +352,16 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
307
352
  >
308
353
  {label}
309
354
  {active ? (
310
- sortConfig!.direction === 'asc' ? <ArrowUp className="w-3 h-3" /> : <ArrowDown className="w-3 h-3" />
355
+ sortConfig!.direction === 'asc' ? (
356
+ <ArrowUp className="w-3 h-3" />
357
+ ) : (
358
+ <ArrowDown className="w-3 h-3" />
359
+ )
311
360
  ) : (
312
361
  <ArrowUpDown className="w-3 h-3 text-gray-400" />
313
362
  )}
314
363
  </button>
315
- );
364
+ )
316
365
  }
317
366
 
318
367
  if (loading) {
@@ -320,7 +369,7 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
320
369
  <div className="p-3 pr-6 sm:p-4 sm:pr-8 flex items-center justify-center h-64">
321
370
  <Loader2 className="w-6 h-6 animate-spin text-blue-600" />
322
371
  </div>
323
- );
372
+ )
324
373
  }
325
374
 
326
375
  return (
@@ -329,7 +378,12 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
329
378
  <div className="mb-4 flex items-center gap-3 rounded-lg border border-red-200 bg-red-50 p-3">
330
379
  <AlertTriangle className="w-5 h-5 text-red-600 shrink-0" />
331
380
  <span className="text-sm text-red-800 flex-1">{error}</span>
332
- <button onClick={refetch} className="px-3 py-1 text-sm text-red-700 border border-red-300 rounded-lg hover:bg-red-100 transition-colors">Retry</button>
381
+ <button
382
+ onClick={refetch}
383
+ className="px-3 py-1 text-sm text-red-700 border border-red-300 rounded-lg hover:bg-red-100 transition-colors"
384
+ >
385
+ Retry
386
+ </button>
333
387
  </div>
334
388
  )}
335
389
 
@@ -337,7 +391,7 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
337
391
  <div className="flex items-center gap-3">
338
392
  <button
339
393
  type="button"
340
- onClick={() => setSidebarOpen(prev => !prev)}
394
+ onClick={() => setSidebarOpen((prev) => !prev)}
341
395
  className="p-1.5 rounded-lg hover:bg-gray-100 transition-colors"
342
396
  title={sidebarOpen ? 'Hide folders' : 'Show folders'}
343
397
  >
@@ -362,7 +416,11 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
362
416
  disabled={uploading}
363
417
  className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm disabled:opacity-50"
364
418
  >
365
- {uploading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Upload className="w-4 h-4" />}
419
+ {uploading ? (
420
+ <Loader2 className="w-4 h-4 animate-spin" />
421
+ ) : (
422
+ <Upload className="w-4 h-4" />
423
+ )}
366
424
  {uploading ? 'Uploading...' : 'Upload Files'}
367
425
  </button>
368
426
  </div>
@@ -374,7 +432,10 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
374
432
  <FolderTree
375
433
  scope="media"
376
434
  selected={folderSel}
377
- onSelect={(sel) => { setFolderSel(sel); setSelectedMedia([]); }}
435
+ onSelect={(sel) => {
436
+ setFolderSel(sel)
437
+ setSelectedMedia([])
438
+ }}
378
439
  totalCount={allData.data?.total}
379
440
  uncategorizedCount={uncatData.data?.total}
380
441
  onDropItem={handleDropItem}
@@ -388,9 +449,19 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
388
449
  <div className="flex items-center gap-3 flex-1">
389
450
  <div className="flex-1 max-w-md relative">
390
451
  <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
391
- <input type="text" placeholder="Search media..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} className="w-full pl-9 pr-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" />
452
+ <input
453
+ type="text"
454
+ placeholder="Search media..."
455
+ value={searchQuery}
456
+ onChange={(e) => setSearchQuery(e.target.value)}
457
+ className="w-full pl-9 pr-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
458
+ />
392
459
  </div>
393
- <select value={filterType} onChange={(e) => setFilterType(e.target.value)} className="px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
460
+ <select
461
+ value={filterType}
462
+ onChange={(e) => setFilterType(e.target.value)}
463
+ className="px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
464
+ >
394
465
  <option value="all">All Types</option>
395
466
  <option value="image">Images</option>
396
467
  <option value="video">Videos</option>
@@ -398,10 +469,16 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
398
469
  </select>
399
470
  </div>
400
471
  <div className="flex items-center gap-1 bg-gray-100 p-1 rounded-lg">
401
- <button onClick={() => setViewMode('grid')} className={`p-1.5 rounded transition-colors ${viewMode === 'grid' ? 'bg-white text-gray-900 shadow-sm' : 'text-gray-600 hover:text-gray-900'}`}>
472
+ <button
473
+ onClick={() => setViewMode('grid')}
474
+ className={`p-1.5 rounded transition-colors ${viewMode === 'grid' ? 'bg-white text-gray-900 shadow-sm' : 'text-gray-600 hover:text-gray-900'}`}
475
+ >
402
476
  <Grid3x3 className="w-4 h-4" />
403
477
  </button>
404
- <button onClick={() => setViewMode('list')} className={`p-1.5 rounded transition-colors ${viewMode === 'list' ? 'bg-white text-gray-900 shadow-sm' : 'text-gray-600 hover:text-gray-900'}`}>
478
+ <button
479
+ onClick={() => setViewMode('list')}
480
+ className={`p-1.5 rounded transition-colors ${viewMode === 'list' ? 'bg-white text-gray-900 shadow-sm' : 'text-gray-600 hover:text-gray-900'}`}
481
+ >
405
482
  <List className="w-4 h-4" />
406
483
  </button>
407
484
  </div>
@@ -411,10 +488,25 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
411
488
  {selectedMedia.length > 0 && (
412
489
  <div className="bg-blue-50 border border-blue-200 rounded-lg p-3 mb-4">
413
490
  <div className="flex items-center justify-between">
414
- <span className="text-sm text-blue-900">{selectedMedia.length} file{selectedMedia.length !== 1 ? 's' : ''} selected</span>
491
+ <span className="text-sm text-blue-900">
492
+ {selectedMedia.length} file{selectedMedia.length !== 1 ? 's' : ''} selected
493
+ </span>
415
494
  <div className="flex items-center gap-2">
416
- <button onClick={async () => { for (const id of selectedMedia) await deleteMedia(id); setSelectedMedia([]); }} className="px-3 py-1.5 text-sm bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors">Delete Selected</button>
417
- <button onClick={() => setSelectedMedia([])} className="px-3 py-1.5 text-sm border border-gray-300 bg-white rounded-lg hover:bg-gray-50 transition-colors">Cancel</button>
495
+ <button
496
+ onClick={async () => {
497
+ for (const id of selectedMedia) await deleteMedia(id)
498
+ setSelectedMedia([])
499
+ }}
500
+ className="px-3 py-1.5 text-sm bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
501
+ >
502
+ Delete Selected
503
+ </button>
504
+ <button
505
+ onClick={() => setSelectedMedia([])}
506
+ className="px-3 py-1.5 text-sm border border-gray-300 bg-white rounded-lg hover:bg-gray-50 transition-colors"
507
+ >
508
+ Cancel
509
+ </button>
418
510
  </div>
419
511
  </div>
420
512
  </div>
@@ -429,377 +521,479 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
429
521
  {folderSel.type === 'smart' && folderSel.smart === 'uncategorized'
430
522
  ? 'No uncategorized media'
431
523
  : folderSel.type === 'folder'
432
- ? 'No media in this folder'
433
- : 'No media yet'}
524
+ ? 'No media in this folder'
525
+ : 'No media yet'}
434
526
  </h3>
435
527
  <p className="text-sm text-gray-500">Upload your first file to get started.</p>
436
528
  </div>
437
529
  ) : (
438
- <div className="flex gap-4 flex-1 overflow-hidden min-h-0">
439
- <div className={`bg-white rounded-lg border border-gray-200 overflow-hidden transition-all duration-200 ${panelOpen ? 'flex-1 min-w-0' : 'w-full'}`}>
440
- {viewMode === 'grid' ? (
441
- <div className={`grid gap-2 sm:gap-3 p-2 sm:p-3 overflow-y-auto h-full ${
442
- panelOpen
443
- ? 'grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'
444
- : 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-8'
445
- }`}>
446
- {filteredAndSorted.map((item) => {
447
- const isActive = activeItem?.id === item.id;
448
- const hasIssues = !item.altTag || !item.title || item.usedOn?.length === 0;
449
- return (
450
- <div
451
- key={item.id}
452
- className={`group relative aspect-square rounded-lg border-2 overflow-hidden cursor-pointer transition-all ${
453
- isActive ? 'border-blue-500 ring-2 ring-blue-200' :
454
- selectedMedia.includes(item.id) ? 'border-blue-400 ring-1 ring-blue-100' :
455
- 'border-gray-200 hover:border-gray-300'
456
- }`}
457
- onClick={() => openDetail(item)}
458
- draggable
459
- onDragStart={(e) => handleDragStart(e, item.id)}
460
- >
461
- <div className="w-full h-full bg-gray-100 flex items-center justify-center">
462
- <MediaPreview item={item} />
463
- </div>
464
- <div className="absolute inset-0 bg-linear-to-t from-black/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity">
465
- <div className="absolute bottom-0 left-0 right-0 p-2">
466
- <p className="text-white text-xs font-medium truncate">{item.name}</p>
467
- <p className="text-white/80 text-xs">{item.size}</p>
468
- </div>
469
- </div>
470
- {hasIssues && (
471
- <div className="absolute top-1.5 left-1.5 w-5 h-5 bg-yellow-500 rounded-full flex items-center justify-center" title="Needs attention">
472
- <AlertTriangle className="w-3 h-3 text-white" />
530
+ <div className="flex gap-4 flex-1 overflow-hidden min-h-0">
531
+ <div
532
+ className={`bg-white rounded-lg border border-gray-200 overflow-hidden transition-all duration-200 ${panelOpen ? 'flex-1 min-w-0' : 'w-full'}`}
533
+ >
534
+ {viewMode === 'grid' ? (
535
+ <div
536
+ className={`grid gap-2 sm:gap-3 p-2 sm:p-3 overflow-y-auto h-full ${
537
+ panelOpen
538
+ ? 'grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'
539
+ : 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-8'
540
+ }`}
541
+ >
542
+ {filteredAndSorted.map((item) => {
543
+ const isActive = activeItem?.id === item.id
544
+ const hasIssues = !item.altTag || !item.title || item.usedOn?.length === 0
545
+ return (
546
+ <div
547
+ key={item.id}
548
+ className={`group relative aspect-square rounded-lg border-2 overflow-hidden cursor-pointer transition-all ${
549
+ isActive
550
+ ? 'border-blue-500 ring-2 ring-blue-200'
551
+ : selectedMedia.includes(item.id)
552
+ ? 'border-blue-400 ring-1 ring-blue-100'
553
+ : 'border-gray-200 hover:border-gray-300'
554
+ }`}
555
+ onClick={() => openDetail(item)}
556
+ draggable
557
+ onDragStart={(e) => handleDragStart(e, item.id)}
558
+ >
559
+ <div className="w-full h-full bg-gray-100 flex items-center justify-center">
560
+ <MediaPreview item={item} />
473
561
  </div>
474
- )}
475
- <div className="absolute top-1.5 right-1.5 opacity-0 group-hover:opacity-100 transition-opacity" onClick={(e) => handleCheckbox(e, item.id)}>
476
- <div className={`w-5 h-5 rounded border-2 flex items-center justify-center transition-colors ${
477
- selectedMedia.includes(item.id) ? 'bg-blue-600 border-blue-600' : 'bg-white/80 border-gray-400'
478
- }`}>
479
- {selectedMedia.includes(item.id) && (
480
- <svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 12 12">
481
- <path d="M10 3L4.5 8.5L2 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
482
- </svg>
483
- )}
562
+ <div className="absolute inset-0 bg-linear-to-t from-black/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity">
563
+ <div className="absolute bottom-0 left-0 right-0 p-2">
564
+ <p className="text-white text-xs font-medium truncate">{item.name}</p>
565
+ <p className="text-white/80 text-xs">{item.size}</p>
566
+ </div>
484
567
  </div>
485
- </div>
486
- </div>
487
- );
488
- })}
489
- </div>
490
- ) : (
491
- <div className="overflow-y-auto h-full">
492
- <table className="w-full">
493
- <thead className="bg-gray-50 border-b border-gray-200 sticky top-0">
494
- <tr>
495
- <th className="w-8 px-3 py-2 text-left"><input type="checkbox" checked={selectedMedia.length === filteredAndSorted.length && filteredAndSorted.length > 0} onChange={handleSelectAll} className="rounded border-gray-300" /></th>
496
- <th className="w-6 px-1 py-2"></th>
497
- <th className="px-3 py-2 text-left"><SortHeader label="Name" sortKey="name" /></th>
498
- <th className="px-3 py-2 text-left"><SortHeader label="Type" sortKey="type" /></th>
499
- <th className="px-3 py-2 text-left"><SortHeader label="Size" sortKey="size" /></th>
500
- <th className="px-3 py-2 text-left"><SortHeader label="Uploaded" sortKey="date" /></th>
501
- <th className="px-3 py-2 text-left text-xs font-medium text-gray-700">Status</th>
502
- </tr>
503
- </thead>
504
- <tbody className="divide-y divide-gray-200">
505
- {filteredAndSorted.map((item) => {
506
- const isActive = activeItem?.id === item.id;
507
- const hasIssues = !item.altTag || !item.title || item.usedOn?.length === 0;
508
- return (
509
- <tr
510
- key={item.id}
511
- className={`transition-colors cursor-pointer ${isActive ? 'bg-blue-50' : 'hover:bg-gray-50'}`}
512
- onClick={() => openDetail(item)}
513
- draggable
514
- onDragStart={(e) => handleDragStart(e, item.id)}
568
+ {hasIssues && (
569
+ <div
570
+ className="absolute top-1.5 left-1.5 w-5 h-5 bg-yellow-500 rounded-full flex items-center justify-center"
571
+ title="Needs attention"
572
+ >
573
+ <AlertTriangle className="w-3 h-3 text-white" />
574
+ </div>
575
+ )}
576
+ <div
577
+ className="absolute top-1.5 right-1.5 opacity-0 group-hover:opacity-100 transition-opacity"
578
+ onClick={(e) => handleCheckbox(e, item.id)}
515
579
  >
516
- <td className="px-3 py-2" onClick={(e) => e.stopPropagation()}>
517
- <input type="checkbox" checked={selectedMedia.includes(item.id)} onChange={() => handleCheckbox({ stopPropagation: () => {} } as React.MouseEvent, item.id)} className="rounded border-gray-300" />
518
- </td>
519
- <td className="px-1 py-2 cursor-grab">
520
- <GripVertical className="w-4 h-4 text-gray-300" />
521
- </td>
522
- <td className="px-3 py-2">
523
- <div className="flex items-center gap-3">
524
- <div className="w-10 h-10 bg-gray-100 rounded flex items-center justify-center overflow-hidden"><MediaPreview item={item} /></div>
525
- <span className="text-sm font-medium text-gray-900">{item.name}</span>
526
- </div>
527
- </td>
528
- <td className="px-3 py-2 text-sm text-gray-600">{item.format ?? item.type}</td>
529
- <td className="px-3 py-2 text-sm text-gray-600">{item.size}</td>
530
- <td className="px-3 py-2 text-sm text-gray-600">{item.date}</td>
531
- <td className="px-3 py-2">
532
- {hasIssues ? (
533
- <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
534
- <AlertTriangle className="w-3 h-3" /> Needs attention
535
- </span>
536
- ) : (
537
- <span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">Complete</span>
580
+ <div
581
+ className={`w-5 h-5 rounded border-2 flex items-center justify-center transition-colors ${
582
+ selectedMedia.includes(item.id)
583
+ ? 'bg-blue-600 border-blue-600'
584
+ : 'bg-white/80 border-gray-400'
585
+ }`}
586
+ >
587
+ {selectedMedia.includes(item.id) && (
588
+ <svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 12 12">
589
+ <path
590
+ d="M10 3L4.5 8.5L2 6"
591
+ stroke="currentColor"
592
+ strokeWidth="2"
593
+ strokeLinecap="round"
594
+ strokeLinejoin="round"
595
+ />
596
+ </svg>
538
597
  )}
539
- </td>
540
- </tr>
541
- );
542
- })}
543
- </tbody>
544
- </table>
545
- </div>
546
- )}
547
- </div>
598
+ </div>
599
+ </div>
600
+ </div>
601
+ )
602
+ })}
603
+ </div>
604
+ ) : (
605
+ <div className="overflow-y-auto h-full">
606
+ <table className="w-full">
607
+ <thead className="bg-gray-50 border-b border-gray-200 sticky top-0">
608
+ <tr>
609
+ <th className="w-8 px-3 py-2 text-left">
610
+ <input
611
+ type="checkbox"
612
+ checked={
613
+ selectedMedia.length === filteredAndSorted.length &&
614
+ filteredAndSorted.length > 0
615
+ }
616
+ onChange={handleSelectAll}
617
+ className="rounded border-gray-300"
618
+ />
619
+ </th>
620
+ <th className="w-6 px-1 py-2"></th>
621
+ <th className="px-3 py-2 text-left">
622
+ <SortHeader label="Name" sortKey="name" />
623
+ </th>
624
+ <th className="px-3 py-2 text-left">
625
+ <SortHeader label="Type" sortKey="type" />
626
+ </th>
627
+ <th className="px-3 py-2 text-left">
628
+ <SortHeader label="Size" sortKey="size" />
629
+ </th>
630
+ <th className="px-3 py-2 text-left">
631
+ <SortHeader label="Uploaded" sortKey="date" />
632
+ </th>
633
+ <th className="px-3 py-2 text-left text-xs font-medium text-gray-700">
634
+ Status
635
+ </th>
636
+ </tr>
637
+ </thead>
638
+ <tbody className="divide-y divide-gray-200">
639
+ {filteredAndSorted.map((item) => {
640
+ const isActive = activeItem?.id === item.id
641
+ const hasIssues = !item.altTag || !item.title || item.usedOn?.length === 0
642
+ return (
643
+ <tr
644
+ key={item.id}
645
+ className={`transition-colors cursor-pointer ${isActive ? 'bg-blue-50' : 'hover:bg-gray-50'}`}
646
+ onClick={() => openDetail(item)}
647
+ draggable
648
+ onDragStart={(e) => handleDragStart(e, item.id)}
649
+ >
650
+ <td className="px-3 py-2" onClick={(e) => e.stopPropagation()}>
651
+ <input
652
+ type="checkbox"
653
+ checked={selectedMedia.includes(item.id)}
654
+ onChange={() =>
655
+ handleCheckbox(
656
+ { stopPropagation: () => {} } as React.MouseEvent,
657
+ item.id,
658
+ )
659
+ }
660
+ className="rounded border-gray-300"
661
+ />
662
+ </td>
663
+ <td className="px-1 py-2 cursor-grab">
664
+ <GripVertical className="w-4 h-4 text-gray-300" />
665
+ </td>
666
+ <td className="px-3 py-2">
667
+ <div className="flex items-center gap-3">
668
+ <div className="w-10 h-10 bg-gray-100 rounded flex items-center justify-center overflow-hidden">
669
+ <MediaPreview item={item} />
670
+ </div>
671
+ <span className="text-sm font-medium text-gray-900">
672
+ {item.name}
673
+ </span>
674
+ </div>
675
+ </td>
676
+ <td className="px-3 py-2 text-sm text-gray-600">
677
+ {item.format ?? item.type}
678
+ </td>
679
+ <td className="px-3 py-2 text-sm text-gray-600">{item.size}</td>
680
+ <td className="px-3 py-2 text-sm text-gray-600">{item.date}</td>
681
+ <td className="px-3 py-2">
682
+ {hasIssues ? (
683
+ <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
684
+ <AlertTriangle className="w-3 h-3" /> Needs attention
685
+ </span>
686
+ ) : (
687
+ <span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
688
+ Complete
689
+ </span>
690
+ )}
691
+ </td>
692
+ </tr>
693
+ )
694
+ })}
695
+ </tbody>
696
+ </table>
697
+ </div>
698
+ )}
699
+ </div>
548
700
 
549
- {panelOpen && activeItem && (
550
- <div className="w-80 lg:w-96 bg-white rounded-lg border border-gray-200 overflow-y-auto shrink-0 flex flex-col">
551
- <div className="flex items-center justify-between p-4 border-b border-gray-200 sticky top-0 bg-white z-10">
552
- <h3 className="text-sm font-semibold text-gray-900 truncate">{activeItem.name}</h3>
553
- <button onClick={closeDetail} className="p-1 hover:bg-gray-100 rounded transition-colors" aria-label="Close panel">
554
- <X className="w-4 h-4 text-gray-500" />
555
- </button>
556
- </div>
701
+ {panelOpen && activeItem && (
702
+ <div className="w-80 lg:w-96 bg-white rounded-lg border border-gray-200 overflow-y-auto shrink-0 flex flex-col">
703
+ <div className="flex items-center justify-between p-4 border-b border-gray-200 sticky top-0 bg-white z-10">
704
+ <h3 className="text-sm font-semibold text-gray-900 truncate">
705
+ {activeItem.name}
706
+ </h3>
707
+ <button
708
+ onClick={closeDetail}
709
+ className="p-1 hover:bg-gray-100 rounded transition-colors"
710
+ aria-label="Close panel"
711
+ >
712
+ <X className="w-4 h-4 text-gray-500" />
713
+ </button>
714
+ </div>
557
715
 
558
- <div className="flex-1 overflow-y-auto">
559
- <div className="p-4 border-b border-gray-200">
560
- <div className="aspect-video bg-gray-100 rounded-lg flex items-center justify-center overflow-hidden">
561
- {isImageMedia(activeItem) ? (
562
- <MediaPreview item={activeItem} />
563
- ) : (
564
- <ImageIcon className="w-12 h-12 text-gray-300" />
565
- )}
716
+ <div className="flex-1 overflow-y-auto">
717
+ <div className="p-4 border-b border-gray-200">
718
+ <div className="aspect-video bg-gray-100 rounded-lg flex items-center justify-center overflow-hidden">
719
+ {isImageMedia(activeItem) ? (
720
+ <MediaPreview item={activeItem} />
721
+ ) : (
722
+ <ImageIcon className="w-12 h-12 text-gray-300" />
723
+ )}
724
+ </div>
566
725
  </div>
567
- </div>
568
726
 
569
- {issues.length > 0 && (
570
- <div className="mx-4 mt-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
571
- <div className="flex items-start gap-2">
572
- <AlertTriangle className="w-4 h-4 text-yellow-600 mt-0.5 shrink-0" />
727
+ {issues.length > 0 && (
728
+ <div className="mx-4 mt-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
729
+ <div className="flex items-start gap-2">
730
+ <AlertTriangle className="w-4 h-4 text-yellow-600 mt-0.5 shrink-0" />
731
+ <div>
732
+ <p className="text-xs font-semibold text-yellow-900 mb-1">
733
+ {issues.length} issue{issues.length !== 1 ? 's' : ''} found
734
+ </p>
735
+ <ul className="space-y-0.5">
736
+ {issues.map((issue, i) => (
737
+ <li key={i} className="text-xs text-yellow-800">
738
+ • {issue}
739
+ </li>
740
+ ))}
741
+ </ul>
742
+ </div>
743
+ </div>
744
+ <button
745
+ type="button"
746
+ onClick={async () => {
747
+ if (!activeItem.altTag) await handleAiGenerate('alt')
748
+ if (!activeItem.title) await handleAiGenerate('title')
749
+ if (activeItem.sizeBytes > 2000000) await handleAiGenerate('optimize')
750
+ }}
751
+ className="mt-2 w-full flex items-center justify-center gap-1.5 px-3 py-1.5 text-xs bg-yellow-600 text-white rounded-lg hover:bg-yellow-700 transition-colors"
752
+ >
753
+ <Sparkles className="w-3.5 h-3.5" />
754
+ AI Fix All Issues
755
+ </button>
756
+ </div>
757
+ )}
758
+
759
+ <div className="p-4 border-b border-gray-200 space-y-3">
760
+ <h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wide">
761
+ File Information
762
+ </h4>
763
+ <div className="grid grid-cols-2 gap-3">
573
764
  <div>
574
- <p className="text-xs font-semibold text-yellow-900 mb-1">{issues.length} issue{issues.length !== 1 ? 's' : ''} found</p>
575
- <ul className="space-y-0.5">
576
- {issues.map((issue, i) => (
577
- <li key={i} className="text-xs text-yellow-800">• {issue}</li>
578
- ))}
579
- </ul>
765
+ <div className="text-xs text-gray-500 mb-0.5">Format</div>
766
+ <div className="text-sm text-gray-900">
767
+ {activeItem.format ?? 'Unknown'}
768
+ </div>
769
+ </div>
770
+ <div>
771
+ <div className="text-xs text-gray-500 mb-0.5">File Size</div>
772
+ <div className="text-sm text-gray-900 flex items-center gap-1">
773
+ {activeItem.size}
774
+ {activeItem.sizeBytes > 2000000 && (
775
+ <span className="text-yellow-600 text-xs">(large)</span>
776
+ )}
777
+ </div>
778
+ </div>
779
+ <div>
780
+ <div className="text-xs text-gray-500 mb-0.5">Dimensions</div>
781
+ <div className="text-sm text-gray-900">
782
+ {activeItem.dimensions ?? '—'}
783
+ </div>
784
+ </div>
785
+ <div>
786
+ <div className="text-xs text-gray-500 mb-0.5">Uploaded</div>
787
+ <div className="text-sm text-gray-900">{activeItem.date}</div>
580
788
  </div>
581
789
  </div>
582
- <button
583
- type="button"
584
- onClick={async () => {
585
- if (!activeItem.altTag) await handleAiGenerate('alt');
586
- if (!activeItem.title) await handleAiGenerate('title');
587
- if (activeItem.sizeBytes > 2000000) await handleAiGenerate('optimize');
588
- }}
589
- className="mt-2 w-full flex items-center justify-center gap-1.5 px-3 py-1.5 text-xs bg-yellow-600 text-white rounded-lg hover:bg-yellow-700 transition-colors"
590
- >
591
- <Sparkles className="w-3.5 h-3.5" />
592
- AI Fix All Issues
593
- </button>
594
- </div>
595
- )}
596
790
 
597
- <div className="p-4 border-b border-gray-200 space-y-3">
598
- <h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wide">File Information</h4>
599
- <div className="grid grid-cols-2 gap-3">
600
791
  <div>
601
- <div className="text-xs text-gray-500 mb-0.5">Format</div>
602
- <div className="text-sm text-gray-900">{activeItem.format ?? 'Unknown'}</div>
792
+ <div className="text-xs text-gray-500 mb-1">URL</div>
793
+ <div className="flex items-center gap-1">
794
+ <code className="flex-1 text-xs bg-gray-50 border border-gray-200 px-2 py-1.5 rounded text-gray-700 truncate">
795
+ {activeItem.url}
796
+ </code>
797
+ <button
798
+ onClick={handleCopyUrl}
799
+ className="p-1.5 hover:bg-gray-100 rounded transition-colors shrink-0"
800
+ title="Copy URL"
801
+ >
802
+ <Copy className="w-3.5 h-3.5 text-gray-500" />
803
+ </button>
804
+ </div>
603
805
  </div>
806
+ </div>
807
+
808
+ <div className="p-4 border-b border-gray-200 space-y-4">
809
+ <h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wide">
810
+ SEO & Accessibility
811
+ </h4>
812
+
604
813
  <div>
605
- <div className="text-xs text-gray-500 mb-0.5">File Size</div>
606
- <div className="text-sm text-gray-900 flex items-center gap-1">
607
- {activeItem.size}
608
- {activeItem.sizeBytes > 2000000 && (
609
- <span className="text-yellow-600 text-xs">(large)</span>
610
- )}
814
+ <div className="flex items-center justify-between mb-1">
815
+ <label className="text-sm font-medium text-gray-700">Alt Tag</label>
816
+ <button
817
+ type="button"
818
+ onClick={() => handleAiGenerate('alt')}
819
+ disabled={aiGenerating === 'alt'}
820
+ className="flex items-center gap-1 text-xs text-indigo-600 hover:text-indigo-700 disabled:opacity-50 transition-colors"
821
+ >
822
+ <Bot className="w-3.5 h-3.5" />
823
+ {aiGenerating === 'alt' ? 'Generating...' : 'AI Generate'}
824
+ </button>
611
825
  </div>
826
+ <textarea
827
+ value={editAlt}
828
+ onChange={(e) => setEditAlt(e.target.value)}
829
+ placeholder="Describe this image for accessibility..."
830
+ rows={2}
831
+ className="w-full text-sm border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
832
+ />
833
+ {!editAlt && (
834
+ <p className="text-xs text-red-500 mt-1">
835
+ Required for accessibility and SEO
836
+ </p>
837
+ )}
612
838
  </div>
839
+
613
840
  <div>
614
- <div className="text-xs text-gray-500 mb-0.5">Dimensions</div>
615
- <div className="text-sm text-gray-900">{activeItem.dimensions ?? '—'}</div>
841
+ <div className="flex items-center justify-between mb-1">
842
+ <label className="text-sm font-medium text-gray-700">Title</label>
843
+ <button
844
+ type="button"
845
+ onClick={() => handleAiGenerate('title')}
846
+ disabled={aiGenerating === 'title'}
847
+ className="flex items-center gap-1 text-xs text-indigo-600 hover:text-indigo-700 disabled:opacity-50 transition-colors"
848
+ >
849
+ <Bot className="w-3.5 h-3.5" />
850
+ {aiGenerating === 'title' ? 'Generating...' : 'AI Generate'}
851
+ </button>
852
+ </div>
853
+ <input
854
+ type="text"
855
+ value={editTitle}
856
+ onChange={(e) => setEditTitle(e.target.value)}
857
+ placeholder="Image title..."
858
+ className="w-full text-sm border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
859
+ />
616
860
  </div>
861
+
617
862
  <div>
618
- <div className="text-xs text-gray-500 mb-0.5">Uploaded</div>
619
- <div className="text-sm text-gray-900">{activeItem.date}</div>
863
+ <label className="text-sm font-medium text-gray-700 mb-1 block">
864
+ File Name
865
+ </label>
866
+ <input
867
+ type="text"
868
+ value={editFilename}
869
+ onChange={(e) => setEditFilename(e.target.value)}
870
+ className="w-full text-sm border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
871
+ />
620
872
  </div>
621
873
  </div>
622
874
 
623
- <div>
624
- <div className="text-xs text-gray-500 mb-1">URL</div>
625
- <div className="flex items-center gap-1">
626
- <code className="flex-1 text-xs bg-gray-50 border border-gray-200 px-2 py-1.5 rounded text-gray-700 truncate">{activeItem.url}</code>
627
- <button onClick={handleCopyUrl} className="p-1.5 hover:bg-gray-100 rounded transition-colors shrink-0" title="Copy URL">
628
- <Copy className="w-3.5 h-3.5 text-gray-500" />
629
- </button>
875
+ {isImageMedia(activeItem) && activeItem.url && (
876
+ <div className="p-4 border-b border-gray-200">
877
+ <FocalPointPicker
878
+ imageUrl={activeItem.url}
879
+ focalX={focalX}
880
+ focalY={focalY}
881
+ onChange={(x, y) => {
882
+ setFocalX(x)
883
+ setFocalY(y)
884
+ }}
885
+ />
630
886
  </div>
631
- </div>
632
- </div>
633
-
634
- <div className="p-4 border-b border-gray-200 space-y-4">
635
- <h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wide">SEO & Accessibility</h4>
887
+ )}
636
888
 
637
- <div>
638
- <div className="flex items-center justify-between mb-1">
639
- <label className="text-sm font-medium text-gray-700">Alt Tag</label>
640
- <button
641
- type="button"
642
- onClick={() => handleAiGenerate('alt')}
643
- disabled={aiGenerating === 'alt'}
644
- className="flex items-center gap-1 text-xs text-indigo-600 hover:text-indigo-700 disabled:opacity-50 transition-colors"
645
- >
646
- <Bot className="w-3.5 h-3.5" />
647
- {aiGenerating === 'alt' ? 'Generating...' : 'AI Generate'}
648
- </button>
649
- </div>
650
- <textarea
651
- value={editAlt}
652
- onChange={(e) => setEditAlt(e.target.value)}
653
- placeholder="Describe this image for accessibility..."
654
- rows={2}
655
- className="w-full text-sm border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
656
- />
657
- {!editAlt && (
658
- <p className="text-xs text-red-500 mt-1">Required for accessibility and SEO</p>
889
+ <div className="p-4 border-b border-gray-200">
890
+ <h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3">
891
+ Used On {activeItem.usedOn && `(${activeItem.usedOn.length})`}
892
+ </h4>
893
+ {activeItem.usedOn && activeItem.usedOn.length > 0 ? (
894
+ <div className="space-y-2">
895
+ {activeItem.usedOn.map((usage, i) => (
896
+ <button
897
+ key={i}
898
+ type="button"
899
+ onClick={() => onNavigate?.(usage.path)}
900
+ className="w-full flex items-center gap-2 p-2 rounded-lg border border-gray-200 hover:bg-gray-50 transition-colors text-left"
901
+ >
902
+ <Link2 className="w-4 h-4 text-gray-400 shrink-0" />
903
+ <span className="text-sm text-gray-900 flex-1 truncate">
904
+ {usage.page}
905
+ </span>
906
+ <ExternalLink className="w-3.5 h-3.5 text-gray-400 shrink-0" />
907
+ </button>
908
+ ))}
909
+ </div>
910
+ ) : (
911
+ <div className="p-3 bg-orange-50 border border-orange-200 rounded-lg">
912
+ <div className="flex items-start gap-2">
913
+ <AlertTriangle className="w-4 h-4 text-orange-600 mt-0.5 shrink-0" />
914
+ <div>
915
+ <p className="text-xs font-medium text-orange-900">Orphaned media</p>
916
+ <p className="text-xs text-orange-700 mt-0.5">
917
+ This file isn&apos;t used on any page. Consider deleting it to save
918
+ storage.
919
+ </p>
920
+ </div>
921
+ </div>
922
+ </div>
659
923
  )}
660
924
  </div>
661
925
 
662
- <div>
663
- <div className="flex items-center justify-between mb-1">
664
- <label className="text-sm font-medium text-gray-700">Title</label>
665
- <button
666
- type="button"
667
- onClick={() => handleAiGenerate('title')}
668
- disabled={aiGenerating === 'title'}
669
- className="flex items-center gap-1 text-xs text-indigo-600 hover:text-indigo-700 disabled:opacity-50 transition-colors"
670
- >
671
- <Bot className="w-3.5 h-3.5" />
672
- {aiGenerating === 'title' ? 'Generating...' : 'AI Generate'}
673
- </button>
674
- </div>
675
- <input
676
- type="text"
677
- value={editTitle}
678
- onChange={(e) => setEditTitle(e.target.value)}
679
- placeholder="Image title..."
680
- className="w-full text-sm border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
681
- />
682
- </div>
683
-
684
- <div>
685
- <label className="text-sm font-medium text-gray-700 mb-1 block">File Name</label>
686
- <input
687
- type="text"
688
- value={editFilename}
689
- onChange={(e) => setEditFilename(e.target.value)}
690
- className="w-full text-sm border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
691
- />
692
- </div>
693
- </div>
694
-
695
- {isImageMedia(activeItem) && activeItem.url && (
696
- <div className="p-4 border-b border-gray-200">
697
- <FocalPointPicker
698
- imageUrl={activeItem.url}
699
- focalX={focalX}
700
- focalY={focalY}
701
- onChange={(x, y) => { setFocalX(x); setFocalY(y); }}
702
- />
703
- </div>
704
- )}
705
-
706
- <div className="p-4 border-b border-gray-200">
707
- <h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3">
708
- Used On {activeItem.usedOn && `(${activeItem.usedOn.length})`}
709
- </h4>
710
- {activeItem.usedOn && activeItem.usedOn.length > 0 ? (
711
- <div className="space-y-2">
712
- {activeItem.usedOn.map((usage, i) => (
713
- <button
714
- key={i}
715
- type="button"
716
- onClick={() => onNavigate?.(usage.path)}
717
- className="w-full flex items-center gap-2 p-2 rounded-lg border border-gray-200 hover:bg-gray-50 transition-colors text-left"
718
- >
719
- <Link2 className="w-4 h-4 text-gray-400 shrink-0" />
720
- <span className="text-sm text-gray-900 flex-1 truncate">{usage.page}</span>
721
- <ExternalLink className="w-3.5 h-3.5 text-gray-400 shrink-0" />
722
- </button>
723
- ))}
724
- </div>
725
- ) : (
726
- <div className="p-3 bg-orange-50 border border-orange-200 rounded-lg">
727
- <div className="flex items-start gap-2">
728
- <AlertTriangle className="w-4 h-4 text-orange-600 mt-0.5 shrink-0" />
729
- <div>
730
- <p className="text-xs font-medium text-orange-900">Orphaned media</p>
731
- <p className="text-xs text-orange-700 mt-0.5">This file isn&apos;t used on any page. Consider deleting it to save storage.</p>
926
+ <div className="p-4 space-y-3">
927
+ <h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wide">
928
+ AI Optimization
929
+ </h4>
930
+ <button
931
+ type="button"
932
+ onClick={() => handleAiGenerate('optimize')}
933
+ disabled={aiGenerating === 'optimize'}
934
+ className="w-full flex items-center gap-2 p-3 rounded-lg border border-indigo-200 bg-indigo-50 hover:bg-indigo-100 transition-colors text-left disabled:opacity-50"
935
+ >
936
+ <Sparkles
937
+ className={`w-5 h-5 text-indigo-600 shrink-0 ${aiGenerating === 'optimize' ? 'animate-spin' : ''}`}
938
+ />
939
+ <div className="flex-1">
940
+ <div className="text-sm font-medium text-indigo-900">
941
+ {aiGenerating === 'optimize' ? 'Optimizing...' : 'Optimize Image'}
942
+ </div>
943
+ <div className="text-xs text-indigo-700 mt-0.5">
944
+ Compress and convert to modern format (WebP/AVIF)
732
945
  </div>
733
946
  </div>
734
- </div>
735
- )}
947
+ </button>
948
+ <button
949
+ type="button"
950
+ className="w-full flex items-center gap-2 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 transition-colors text-left"
951
+ >
952
+ <Bot className="w-5 h-5 text-gray-500 shrink-0" />
953
+ <div className="flex-1">
954
+ <div className="text-sm font-medium text-gray-900">
955
+ AI Content Analysis
956
+ </div>
957
+ <div className="text-xs text-gray-600 mt-0.5">
958
+ Detect objects, faces, text, and suggest categories
959
+ </div>
960
+ </div>
961
+ </button>
962
+ </div>
736
963
  </div>
737
964
 
738
- <div className="p-4 space-y-3">
739
- <h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wide">AI Optimization</h4>
965
+ <div className="p-4 border-t border-gray-200 bg-white sticky bottom-0 flex items-center gap-2">
740
966
  <button
741
967
  type="button"
742
- onClick={() => handleAiGenerate('optimize')}
743
- disabled={aiGenerating === 'optimize'}
744
- className="w-full flex items-center gap-2 p-3 rounded-lg border border-indigo-200 bg-indigo-50 hover:bg-indigo-100 transition-colors text-left disabled:opacity-50"
968
+ onClick={handleSaveDetails}
969
+ disabled={saving}
970
+ className="flex-1 px-4 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
745
971
  >
746
- <Sparkles className={`w-5 h-5 text-indigo-600 shrink-0 ${aiGenerating === 'optimize' ? 'animate-spin' : ''}`} />
747
- <div className="flex-1">
748
- <div className="text-sm font-medium text-indigo-900">
749
- {aiGenerating === 'optimize' ? 'Optimizing...' : 'Optimize Image'}
750
- </div>
751
- <div className="text-xs text-indigo-700 mt-0.5">
752
- Compress and convert to modern format (WebP/AVIF)
753
- </div>
754
- </div>
972
+ {saving && <Loader2 className="w-4 h-4 animate-spin" />}
973
+ {saving ? 'Saving...' : 'Save Changes'}
755
974
  </button>
756
975
  <button
757
976
  type="button"
758
- className="w-full flex items-center gap-2 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 transition-colors text-left"
977
+ className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
978
+ title="Download"
759
979
  >
760
- <Bot className="w-5 h-5 text-gray-500 shrink-0" />
761
- <div className="flex-1">
762
- <div className="text-sm font-medium text-gray-900">AI Content Analysis</div>
763
- <div className="text-xs text-gray-600 mt-0.5">
764
- Detect objects, faces, text, and suggest categories
765
- </div>
766
- </div>
980
+ <Download className="w-4 h-4 text-gray-600" />
981
+ </button>
982
+ <button
983
+ type="button"
984
+ onClick={() => deleteMedia(activeItem.id)}
985
+ className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
986
+ title="Delete"
987
+ >
988
+ <Trash2 className="w-4 h-4 text-red-600" />
767
989
  </button>
768
990
  </div>
769
991
  </div>
770
-
771
- <div className="p-4 border-t border-gray-200 bg-white sticky bottom-0 flex items-center gap-2">
772
- <button
773
- type="button"
774
- onClick={handleSaveDetails}
775
- disabled={saving}
776
- className="flex-1 px-4 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
777
- >
778
- {saving && <Loader2 className="w-4 h-4 animate-spin" />}
779
- {saving ? 'Saving...' : 'Save Changes'}
780
- </button>
781
- <button
782
- type="button"
783
- className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
784
- title="Download"
785
- >
786
- <Download className="w-4 h-4 text-gray-600" />
787
- </button>
788
- <button
789
- type="button"
790
- onClick={() => deleteMedia(activeItem.id)}
791
- className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
792
- title="Delete"
793
- >
794
- <Trash2 className="w-4 h-4 text-red-600" />
795
- </button>
796
- </div>
797
- </div>
798
- )}
799
- </div>
992
+ )}
993
+ </div>
800
994
  )}
801
995
  </div>
802
996
  </div>
803
997
  </div>
804
- );
998
+ )
805
999
  }