@xleddyl/nuxt-cms 0.9.3 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (433) hide show
  1. package/README.md +1 -1
  2. package/dist/module.d.mts +20 -1
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +1037 -148
  5. package/dist/runtime/app/blocks/CmsBlocks.d.vue.ts +1 -1
  6. package/dist/runtime/app/blocks/CmsBlocks.vue +57 -9
  7. package/dist/runtime/app/blocks/CmsBlocks.vue.d.ts +1 -1
  8. package/dist/runtime/app/blocks/CmsField.d.ts +24 -0
  9. package/dist/runtime/app/blocks/CmsField.js +16 -0
  10. package/dist/runtime/app/blocks/node.d.ts +37 -0
  11. package/dist/runtime/app/blocks/node.js +17 -0
  12. package/dist/runtime/app/blocks/resolve.d.ts +18 -0
  13. package/dist/runtime/app/blocks/resolve.js +34 -0
  14. package/dist/runtime/app/components/cms/BlockPicker.d.vue.ts +22 -0
  15. package/dist/runtime/app/components/cms/BlockPicker.vue +87 -0
  16. package/dist/runtime/app/components/cms/BlockPicker.vue.d.ts +22 -0
  17. package/dist/runtime/app/components/cms/BlockPickerPopover.d.vue.ts +38 -0
  18. package/dist/runtime/app/components/cms/BlockPickerPopover.vue +44 -0
  19. package/dist/runtime/app/components/cms/BlockPickerPopover.vue.d.ts +38 -0
  20. package/dist/runtime/app/components/cms/BlockTree.d.vue.ts +29 -0
  21. package/dist/runtime/app/components/cms/BlockTree.vue +322 -0
  22. package/dist/runtime/app/components/cms/BlockTree.vue.d.ts +29 -0
  23. package/dist/runtime/app/components/cms/BlocksField.vue +569 -218
  24. package/dist/runtime/app/components/cms/BrandMark.vue +7 -13
  25. package/dist/runtime/app/components/cms/ConfirmGroups.vue +15 -13
  26. package/dist/runtime/app/components/cms/{BlockPalette.vue.d.ts → EditorHistory.d.vue.ts} +6 -5
  27. package/dist/runtime/app/components/cms/EditorHistory.vue +34 -0
  28. package/dist/runtime/app/components/cms/{BlockPalette.d.vue.ts → EditorHistory.vue.d.ts} +6 -5
  29. package/dist/runtime/app/components/cms/EntryDrawer.d.vue.ts +11 -5
  30. package/dist/runtime/app/components/cms/EntryDrawer.vue +183 -152
  31. package/dist/runtime/app/components/cms/EntryDrawer.vue.d.ts +11 -5
  32. package/dist/runtime/app/components/cms/EntryForm.d.vue.ts +16 -4
  33. package/dist/runtime/app/components/cms/EntryForm.vue +106 -146
  34. package/dist/runtime/app/components/cms/EntryForm.vue.d.ts +16 -4
  35. package/dist/runtime/app/components/cms/FieldInput.vue +10 -2
  36. package/dist/runtime/app/components/cms/FormSections.d.vue.ts +22 -0
  37. package/dist/runtime/app/components/cms/FormSections.vue +71 -0
  38. package/dist/runtime/app/components/cms/FormSections.vue.d.ts +22 -0
  39. package/dist/runtime/app/components/cms/ImportModal.vue +46 -58
  40. package/dist/runtime/app/components/cms/LinkField.d.vue.ts +16 -0
  41. package/dist/runtime/app/components/cms/LinkField.vue +250 -0
  42. package/dist/runtime/app/components/cms/LinkField.vue.d.ts +16 -0
  43. package/dist/runtime/app/components/cms/ListFilters.vue +10 -12
  44. package/dist/runtime/app/components/cms/LocaleSwitch.d.vue.ts +2 -1
  45. package/dist/runtime/app/components/cms/LocaleSwitch.vue +29 -5
  46. package/dist/runtime/app/components/cms/LocaleSwitch.vue.d.ts +2 -1
  47. package/dist/runtime/app/components/cms/MediaDetail.d.vue.ts +2 -0
  48. package/dist/runtime/app/components/cms/MediaDetail.vue +112 -83
  49. package/dist/runtime/app/components/cms/MediaDetail.vue.d.ts +2 -0
  50. package/dist/runtime/app/components/cms/MediaField.vue +35 -25
  51. package/dist/runtime/app/components/cms/MediaFolderPicker.vue +26 -71
  52. package/dist/runtime/app/components/cms/MediaGallery.d.vue.ts +1 -0
  53. package/dist/runtime/app/components/cms/MediaGallery.vue +442 -419
  54. package/dist/runtime/app/components/cms/MediaGallery.vue.d.ts +1 -0
  55. package/dist/runtime/app/components/cms/MediaThumb.vue +9 -25
  56. package/dist/runtime/app/components/cms/MediaUpload.vue +1 -1
  57. package/dist/runtime/app/components/cms/{BlockOutline.d.vue.ts → NestedBlockList.d.vue.ts} +5 -5
  58. package/dist/runtime/app/components/cms/NestedBlockList.vue +87 -0
  59. package/dist/runtime/app/components/cms/{BlockOutline.vue.d.ts → NestedBlockList.vue.d.ts} +5 -5
  60. package/dist/runtime/app/components/cms/PoweredBy.vue +12 -5
  61. package/dist/runtime/app/components/cms/PresetModal.d.vue.ts +18 -0
  62. package/dist/runtime/app/components/cms/PresetModal.vue +70 -0
  63. package/dist/runtime/app/components/cms/PresetModal.vue.d.ts +18 -0
  64. package/dist/runtime/app/components/cms/RelationField.vue +2 -8
  65. package/dist/runtime/app/components/cms/SettingsAccount.vue +60 -71
  66. package/dist/runtime/app/components/cms/SettingsColumns.vue +47 -69
  67. package/dist/runtime/app/components/cms/SettingsEmptyAdd.d.vue.ts +13 -0
  68. package/dist/runtime/app/components/cms/SettingsEmptyAdd.vue +21 -0
  69. package/dist/runtime/app/components/cms/SettingsEmptyAdd.vue.d.ts +13 -0
  70. package/dist/runtime/app/components/cms/SettingsEntries.d.vue.ts +13 -0
  71. package/dist/runtime/app/components/cms/SettingsEntries.vue +50 -0
  72. package/dist/runtime/app/components/cms/SettingsEntries.vue.d.ts +13 -0
  73. package/dist/runtime/app/components/cms/SettingsEntry.d.vue.ts +1 -10
  74. package/dist/runtime/app/components/cms/SettingsEntry.vue +11 -95
  75. package/dist/runtime/app/components/cms/SettingsEntry.vue.d.ts +1 -10
  76. package/dist/runtime/app/components/cms/SettingsEntryActions.d.vue.ts +7 -0
  77. package/dist/runtime/app/components/cms/SettingsEntryActions.vue +42 -0
  78. package/dist/runtime/app/components/cms/SettingsEntryActions.vue.d.ts +7 -0
  79. package/dist/runtime/app/components/cms/SettingsEntryTabs.d.vue.ts +7 -0
  80. package/dist/runtime/app/components/cms/SettingsEntryTabs.vue +21 -0
  81. package/dist/runtime/app/components/cms/SettingsEntryTabs.vue.d.ts +7 -0
  82. package/dist/runtime/app/components/cms/SettingsFormAction.d.vue.ts +3 -0
  83. package/dist/runtime/app/components/cms/SettingsFormAction.vue +17 -0
  84. package/dist/runtime/app/components/cms/SettingsFormAction.vue.d.ts +3 -0
  85. package/dist/runtime/app/components/cms/SettingsLayout.vue +18 -14
  86. package/dist/runtime/app/components/cms/SettingsPresetForm.d.vue.ts +11 -0
  87. package/dist/runtime/app/components/cms/SettingsPresetForm.vue +60 -0
  88. package/dist/runtime/app/components/cms/SettingsPresetForm.vue.d.ts +11 -0
  89. package/dist/runtime/app/components/cms/SettingsPresets.d.vue.ts +13 -0
  90. package/dist/runtime/app/components/cms/SettingsPresets.vue +65 -0
  91. package/dist/runtime/app/components/cms/SettingsPresets.vue.d.ts +13 -0
  92. package/dist/runtime/app/components/cms/SettingsRecordActions.d.vue.ts +32 -0
  93. package/dist/runtime/app/components/cms/SettingsRecordActions.vue +45 -0
  94. package/dist/runtime/app/components/cms/SettingsRecordActions.vue.d.ts +32 -0
  95. package/dist/runtime/app/components/cms/SettingsRedirectActions.d.vue.ts +3 -0
  96. package/dist/runtime/app/components/cms/SettingsRedirectActions.vue +59 -0
  97. package/dist/runtime/app/components/cms/SettingsRedirectActions.vue.d.ts +3 -0
  98. package/dist/runtime/app/components/cms/SettingsRedirectForm.d.vue.ts +11 -0
  99. package/dist/runtime/app/components/cms/SettingsRedirectForm.vue +77 -0
  100. package/dist/runtime/app/components/cms/SettingsRedirectForm.vue.d.ts +11 -0
  101. package/dist/runtime/app/components/cms/SettingsRedirects.d.vue.ts +13 -0
  102. package/dist/runtime/app/components/cms/SettingsRedirects.vue +117 -0
  103. package/dist/runtime/app/components/cms/SettingsRedirects.vue.d.ts +13 -0
  104. package/dist/runtime/app/components/cms/SettingsSecretCard.d.vue.ts +17 -0
  105. package/dist/runtime/app/components/cms/SettingsSecretCard.vue +43 -0
  106. package/dist/runtime/app/components/cms/SettingsSecretCard.vue.d.ts +17 -0
  107. package/dist/runtime/app/components/cms/SettingsSite.d.vue.ts +3 -0
  108. package/dist/runtime/app/components/cms/SettingsSite.vue +140 -0
  109. package/dist/runtime/app/components/cms/SettingsSite.vue.d.ts +3 -0
  110. package/dist/runtime/app/components/cms/SettingsUserCreate.d.vue.ts +8 -0
  111. package/dist/runtime/app/components/cms/SettingsUserCreate.vue +82 -0
  112. package/dist/runtime/app/components/cms/SettingsUserCreate.vue.d.ts +8 -0
  113. package/dist/runtime/app/components/cms/SettingsUserDetail.d.vue.ts +11 -0
  114. package/dist/runtime/app/components/cms/SettingsUserDetail.vue +179 -0
  115. package/dist/runtime/app/components/cms/SettingsUserDetail.vue.d.ts +11 -0
  116. package/dist/runtime/app/components/cms/SettingsUsers.d.vue.ts +9 -6
  117. package/dist/runtime/app/components/cms/SettingsUsers.vue +88 -234
  118. package/dist/runtime/app/components/cms/SettingsUsers.vue.d.ts +9 -6
  119. package/dist/runtime/app/components/cms/SharePreview.d.vue.ts +18 -0
  120. package/dist/runtime/app/components/cms/SharePreview.vue +219 -0
  121. package/dist/runtime/app/components/cms/SharePreview.vue.d.ts +18 -0
  122. package/dist/runtime/app/components/cms/SlugField.vue +5 -3
  123. package/dist/runtime/app/components/cms/StatusBadge.vue +9 -8
  124. package/dist/runtime/app/components/cms/TranslationBadges.d.vue.ts +7 -0
  125. package/dist/runtime/app/components/cms/TranslationBadges.vue +45 -0
  126. package/dist/runtime/app/components/cms/TranslationBadges.vue.d.ts +7 -0
  127. package/dist/runtime/app/components/cms/TranslationMenu.d.vue.ts +17 -0
  128. package/dist/runtime/app/components/cms/TranslationMenu.vue +28 -0
  129. package/dist/runtime/app/components/cms/TranslationMenu.vue.d.ts +17 -0
  130. package/dist/runtime/app/components/cms/VisualEditor.d.vue.ts +22 -4
  131. package/dist/runtime/app/components/cms/VisualEditor.vue +1152 -335
  132. package/dist/runtime/app/components/cms/VisualEditor.vue.d.ts +22 -4
  133. package/dist/runtime/app/components/cms/WelcomeModal.vue +32 -50
  134. package/dist/runtime/app/composables/cms-clipboard.d.ts +8 -0
  135. package/dist/runtime/app/composables/cms-clipboard.js +38 -0
  136. package/dist/runtime/app/composables/cms-confirm.d.ts +6 -8
  137. package/dist/runtime/app/composables/cms-confirm.js +5 -7
  138. package/dist/runtime/app/composables/cms-entry-disabled.d.ts +5 -1
  139. package/dist/runtime/app/composables/cms-entry-disabled.js +7 -0
  140. package/dist/runtime/app/composables/cms-entry-form.d.ts +15 -0
  141. package/dist/runtime/app/composables/cms-entry-form.js +40 -0
  142. package/dist/runtime/app/composables/cms-entry-settings.d.ts +30 -0
  143. package/dist/runtime/app/composables/cms-entry-settings.js +122 -0
  144. package/dist/runtime/app/composables/cms-entry.d.ts +5 -1
  145. package/dist/runtime/app/composables/cms-entry.js +36 -11
  146. package/dist/runtime/app/composables/cms-history.d.ts +10 -0
  147. package/dist/runtime/app/composables/cms-history.js +24 -0
  148. package/dist/runtime/app/composables/cms-link-targets.d.ts +10 -0
  149. package/dist/runtime/app/composables/cms-link-targets.js +40 -0
  150. package/dist/runtime/app/composables/cms-link-usage.d.ts +1 -0
  151. package/dist/runtime/app/composables/cms-link-usage.js +45 -0
  152. package/dist/runtime/app/composables/cms-media-library.d.ts +19 -9
  153. package/dist/runtime/app/composables/cms-media-library.js +31 -13
  154. package/dist/runtime/app/composables/cms-media-uploader.js +6 -20
  155. package/dist/runtime/app/composables/cms-permissions.d.ts +18 -0
  156. package/dist/runtime/app/composables/cms-permissions.js +54 -0
  157. package/dist/runtime/app/composables/cms-presets.d.ts +11 -0
  158. package/dist/runtime/app/composables/cms-presets.js +40 -0
  159. package/dist/runtime/app/composables/cms-preview-mode.d.ts +4 -0
  160. package/dist/runtime/app/composables/cms-preview-mode.js +8 -0
  161. package/dist/runtime/app/composables/cms-query.d.ts +3 -0
  162. package/dist/runtime/app/composables/cms-query.js +45 -20
  163. package/dist/runtime/app/composables/cms-record-list.d.ts +31 -0
  164. package/dist/runtime/app/composables/cms-record-list.js +75 -0
  165. package/dist/runtime/app/composables/cms-route.d.ts +6 -0
  166. package/dist/runtime/app/composables/cms-route.js +36 -0
  167. package/dist/runtime/app/composables/cms-runtime.d.ts +1 -0
  168. package/dist/runtime/app/composables/cms-save-handoff.d.ts +4 -0
  169. package/dist/runtime/app/composables/cms-save-handoff.js +19 -0
  170. package/dist/runtime/app/composables/cms-seo.d.ts +12 -0
  171. package/dist/runtime/app/composables/cms-seo.js +69 -0
  172. package/dist/runtime/app/composables/cms-settings-form.d.ts +13 -0
  173. package/dist/runtime/app/composables/cms-settings-form.js +29 -0
  174. package/dist/runtime/app/composables/cms-settings-sections.d.ts +26 -0
  175. package/dist/runtime/app/composables/cms-settings-sections.js +181 -0
  176. package/dist/runtime/app/composables/cms-settings-stores.d.ts +52 -0
  177. package/dist/runtime/app/composables/cms-settings-stores.js +156 -0
  178. package/dist/runtime/app/composables/cms-settings.d.ts +3 -3
  179. package/dist/runtime/app/composables/cms-settings.js +14 -8
  180. package/dist/runtime/app/composables/cms-site-disabled.d.ts +7 -0
  181. package/dist/runtime/app/composables/cms-site-disabled.js +8 -0
  182. package/dist/runtime/app/composables/cms-site.d.ts +7 -0
  183. package/dist/runtime/app/composables/cms-site.js +22 -0
  184. package/dist/runtime/app/composables/cms-toast.d.ts +2 -8
  185. package/dist/runtime/app/composables/cms-toast.js +3 -9
  186. package/dist/runtime/app/composables/cms-translation-actions.d.ts +15 -0
  187. package/dist/runtime/app/composables/cms-translation-actions.js +108 -0
  188. package/dist/runtime/app/css-url.d.ts +4 -0
  189. package/dist/runtime/app/image/CmsImage.d.vue.ts +89 -0
  190. package/dist/runtime/app/image/CmsImage.vue +91 -0
  191. package/dist/runtime/app/image/CmsImage.vue.d.ts +89 -0
  192. package/dist/runtime/app/layouts/cms-admin.d.vue.ts +2 -2
  193. package/dist/runtime/app/layouts/cms-admin.vue +98 -76
  194. package/dist/runtime/app/layouts/cms-admin.vue.d.ts +2 -2
  195. package/dist/runtime/app/layouts/cms-editor.vue +7 -2
  196. package/dist/runtime/app/links/CmsLink.d.vue.ts +18 -0
  197. package/dist/runtime/app/links/CmsLink.vue +18 -0
  198. package/dist/runtime/app/links/CmsLink.vue.d.ts +18 -0
  199. package/dist/runtime/app/middleware/cms-auth.js +13 -1
  200. package/dist/runtime/app/pages/admin-collection.vue +473 -297
  201. package/dist/runtime/app/pages/admin-content.vue +190 -53
  202. package/dist/runtime/app/pages/admin-entry.vue +79 -75
  203. package/dist/runtime/app/pages/admin-index.vue +18 -5
  204. package/dist/runtime/app/pages/admin-login.vue +1 -1
  205. package/dist/runtime/app/pages/admin-logout.vue +1 -1
  206. package/dist/runtime/app/pages/admin-media.vue +3 -3
  207. package/dist/runtime/app/pages/admin-preview.vue +112 -24
  208. package/dist/runtime/app/plugins/cms-field.d.ts +2 -0
  209. package/dist/runtime/app/plugins/cms-field.js +8 -0
  210. package/dist/runtime/app/plugins/cms-preview-banner.client.d.ts +2 -0
  211. package/dist/runtime/app/plugins/cms-preview-banner.client.js +19 -0
  212. package/dist/runtime/app/plugins/cms-preview.d.ts +2 -0
  213. package/dist/runtime/app/plugins/cms-preview.js +27 -0
  214. package/dist/runtime/app/preview/CmsPreviewBanner.d.vue.ts +7 -0
  215. package/dist/runtime/app/preview/CmsPreviewBanner.vue +63 -0
  216. package/dist/runtime/app/preview/CmsPreviewBanner.vue.d.ts +7 -0
  217. package/dist/runtime/app/preview/PreviewOverlay.d.vue.ts +17 -3
  218. package/dist/runtime/app/preview/PreviewOverlay.vue +629 -166
  219. package/dist/runtime/app/preview/PreviewOverlay.vue.d.ts +17 -3
  220. package/dist/runtime/app/preview/field-marker.d.ts +12 -0
  221. package/dist/runtime/app/preview/field-marker.js +20 -0
  222. package/dist/runtime/app/preview/inline-text.d.ts +15 -0
  223. package/dist/runtime/app/preview/inline-text.js +158 -0
  224. package/dist/runtime/app/utils/block-editor.d.ts +22 -0
  225. package/dist/runtime/app/utils/block-editor.js +12 -0
  226. package/dist/runtime/app/utils/keyboard.d.ts +6 -0
  227. package/dist/runtime/app/utils/keyboard.js +52 -0
  228. package/dist/runtime/app/utils/redirects.d.ts +6 -0
  229. package/dist/runtime/app/utils/redirects.js +7 -0
  230. package/dist/runtime/app/utils/ui.d.ts +8 -0
  231. package/dist/runtime/app/utils/ui.js +15 -1
  232. package/dist/runtime/app/utils/users.d.ts +15 -0
  233. package/dist/runtime/app/utils/users.js +14 -0
  234. package/dist/runtime/assets/cms.css +1 -1
  235. package/dist/runtime/generate.js +16 -2
  236. package/dist/runtime/server/api/collection-export.get.js +3 -2
  237. package/dist/runtime/server/api/collection-import.post.js +39 -11
  238. package/dist/runtime/server/api/collection.get.js +29 -10
  239. package/dist/runtime/server/api/collection.post.js +28 -11
  240. package/dist/runtime/server/api/collection.put.js +35 -13
  241. package/dist/runtime/server/api/item.delete.js +19 -7
  242. package/dist/runtime/server/api/item.get.js +3 -2
  243. package/dist/runtime/server/api/item.put.js +52 -18
  244. package/dist/runtime/server/api/link-usage.post.d.ts +5 -0
  245. package/dist/runtime/server/api/link-usage.post.js +21 -0
  246. package/dist/runtime/server/api/media-delete.post.js +4 -2
  247. package/dist/runtime/server/api/media-download.get.js +2 -2
  248. package/dist/runtime/server/api/media-folder.delete.js +8 -2
  249. package/dist/runtime/server/api/media-folder.patch.js +10 -2
  250. package/dist/runtime/server/api/media-folder.post.js +4 -2
  251. package/dist/runtime/server/api/media-move.post.js +7 -4
  252. package/dist/runtime/server/api/media-presign.post.js +2 -2
  253. package/dist/runtime/server/api/media-upload.put.js +2 -2
  254. package/dist/runtime/server/api/media-usage.post.d.ts +2 -4
  255. package/dist/runtime/server/api/media-usage.post.js +13 -9
  256. package/dist/runtime/server/api/media.delete.js +4 -2
  257. package/dist/runtime/server/api/media.get.js +22 -7
  258. package/dist/runtime/server/api/media.post.js +4 -2
  259. package/dist/runtime/server/api/media.put.js +33 -16
  260. package/dist/runtime/server/api/preset.delete.d.ts +4 -0
  261. package/dist/runtime/server/api/preset.delete.js +9 -0
  262. package/dist/runtime/server/api/preset.put.d.ts +3 -0
  263. package/dist/runtime/server/api/preset.put.js +23 -0
  264. package/dist/runtime/server/api/presets-field.get.d.ts +3 -0
  265. package/dist/runtime/server/api/presets-field.get.js +13 -0
  266. package/dist/runtime/server/api/presets-field.post.d.ts +3 -0
  267. package/dist/runtime/server/api/presets-field.post.js +36 -0
  268. package/dist/runtime/server/api/presets.get.d.ts +3 -0
  269. package/dist/runtime/server/api/presets.get.js +7 -0
  270. package/dist/runtime/server/api/preview-link.delete.d.ts +4 -0
  271. package/dist/runtime/server/api/preview-link.delete.js +15 -0
  272. package/dist/runtime/server/api/preview-links.get.d.ts +3 -0
  273. package/dist/runtime/server/api/preview-links.get.js +10 -0
  274. package/dist/runtime/server/api/preview-links.post.d.ts +3 -0
  275. package/dist/runtime/server/api/preview-links.post.js +17 -0
  276. package/dist/runtime/server/api/preview.get.d.ts +3 -0
  277. package/dist/runtime/server/api/preview.get.js +7 -0
  278. package/dist/runtime/server/api/redirect.delete.d.ts +4 -0
  279. package/dist/runtime/server/api/redirect.delete.js +10 -0
  280. package/dist/runtime/server/api/redirect.put.d.ts +3 -0
  281. package/dist/runtime/server/api/redirect.put.js +8 -0
  282. package/dist/runtime/server/api/redirects-import.post.d.ts +3 -0
  283. package/dist/runtime/server/api/redirects-import.post.js +13 -0
  284. package/dist/runtime/server/api/redirects.get.d.ts +3 -0
  285. package/dist/runtime/server/api/redirects.get.js +7 -0
  286. package/dist/runtime/server/api/redirects.post.d.ts +3 -0
  287. package/dist/runtime/server/api/redirects.post.js +7 -0
  288. package/dist/runtime/server/api/search.get.js +8 -3
  289. package/dist/runtime/server/api/settings.delete.js +2 -2
  290. package/dist/runtime/server/api/settings.get.js +4 -2
  291. package/dist/runtime/server/api/settings.put.js +2 -2
  292. package/dist/runtime/server/api/site.get.d.ts +3 -0
  293. package/dist/runtime/server/api/site.get.js +7 -0
  294. package/dist/runtime/server/api/site.put.d.ts +3 -0
  295. package/dist/runtime/server/api/site.put.js +16 -0
  296. package/dist/runtime/server/api/tags.get.js +3 -2
  297. package/dist/runtime/server/api/user.delete.js +8 -2
  298. package/dist/runtime/server/api/user.put.js +14 -4
  299. package/dist/runtime/server/api/users.get.js +2 -2
  300. package/dist/runtime/server/api/users.post.js +8 -4
  301. package/dist/runtime/server/middleware/hidden-site.d.ts +2 -0
  302. package/dist/runtime/server/middleware/hidden-site.js +8 -0
  303. package/dist/runtime/server/middleware/preview-links.d.ts +2 -0
  304. package/dist/runtime/server/middleware/preview-links.js +6 -0
  305. package/dist/runtime/server/middleware/redirects.d.ts +2 -0
  306. package/dist/runtime/server/middleware/redirects.js +18 -0
  307. package/dist/runtime/server/plugins/session-validate.js +4 -1
  308. package/dist/runtime/server/plugins/webhooks.d.ts +3 -0
  309. package/dist/runtime/server/plugins/webhooks.js +27 -0
  310. package/dist/runtime/server/routes/graphql.js +5 -3
  311. package/dist/runtime/server/routes/robots.d.ts +2 -0
  312. package/dist/runtime/server/routes/robots.js +16 -0
  313. package/dist/runtime/server/routes/sitemap.d.ts +2 -0
  314. package/dist/runtime/server/routes/sitemap.js +67 -0
  315. package/dist/runtime/server/utils/cached-value.d.ts +5 -0
  316. package/dist/runtime/server/utils/cached-value.js +24 -0
  317. package/dist/runtime/server/utils/content-routes.d.ts +5 -0
  318. package/dist/runtime/server/utils/content-routes.js +29 -0
  319. package/dist/runtime/server/utils/field-rules.d.ts +19 -0
  320. package/dist/runtime/server/utils/field-rules.js +72 -0
  321. package/dist/runtime/server/utils/graphql.d.ts +3 -1
  322. package/dist/runtime/server/utils/graphql.js +145 -29
  323. package/dist/runtime/server/utils/hooks.d.ts +14 -0
  324. package/dist/runtime/server/utils/hooks.js +71 -0
  325. package/dist/runtime/server/utils/import.d.ts +7 -2
  326. package/dist/runtime/server/utils/import.js +108 -14
  327. package/dist/runtime/server/utils/link-usage.d.ts +4 -0
  328. package/dist/runtime/server/utils/link-usage.js +28 -0
  329. package/dist/runtime/server/utils/media-references.d.ts +6 -1
  330. package/dist/runtime/server/utils/media-references.js +49 -20
  331. package/dist/runtime/server/utils/media-usage.d.ts +2 -0
  332. package/dist/runtime/server/utils/media-usage.js +5 -3
  333. package/dist/runtime/server/utils/page-rows.d.ts +2 -0
  334. package/dist/runtime/server/utils/page-rows.js +42 -29
  335. package/dist/runtime/server/utils/permissions.d.ts +14 -0
  336. package/dist/runtime/server/utils/permissions.js +43 -0
  337. package/dist/runtime/server/utils/presets.d.ts +18 -0
  338. package/dist/runtime/server/utils/presets.js +68 -0
  339. package/dist/runtime/server/utils/preview-links.d.ts +207 -0
  340. package/dist/runtime/server/utils/preview-links.js +161 -0
  341. package/dist/runtime/server/utils/redirects.d.ts +31 -0
  342. package/dist/runtime/server/utils/redirects.js +240 -0
  343. package/dist/runtime/server/utils/registry.js +4 -1
  344. package/dist/runtime/server/utils/relations.d.ts +8 -0
  345. package/dist/runtime/server/utils/relations.js +25 -6
  346. package/dist/runtime/server/utils/require-admin.d.ts +0 -1
  347. package/dist/runtime/server/utils/require-admin.js +4 -8
  348. package/dist/runtime/server/utils/search.d.ts +1 -0
  349. package/dist/runtime/server/utils/search.js +44 -19
  350. package/dist/runtime/server/utils/single-row.d.ts +3 -0
  351. package/dist/runtime/server/utils/single-row.js +4 -0
  352. package/dist/runtime/server/utils/site.d.ts +23 -0
  353. package/dist/runtime/server/utils/site.js +79 -0
  354. package/dist/runtime/server/utils/sitemap.d.ts +12 -0
  355. package/dist/runtime/server/utils/sitemap.js +109 -0
  356. package/dist/runtime/server/utils/user-management.d.ts +4 -0
  357. package/dist/runtime/server/utils/user-management.js +20 -0
  358. package/dist/runtime/server/utils/users.d.ts +1 -1
  359. package/dist/runtime/server/utils/users.js +1 -1
  360. package/dist/runtime/server/utils/visibility.d.ts +1 -0
  361. package/dist/runtime/server/utils/visibility.js +6 -1
  362. package/dist/runtime/server/utils/webhooks.d.ts +24 -0
  363. package/dist/runtime/server/utils/webhooks.js +90 -0
  364. package/dist/runtime/shared/block-paths.d.ts +66 -0
  365. package/dist/runtime/shared/block-paths.js +310 -0
  366. package/dist/runtime/shared/block-picker.d.ts +18 -0
  367. package/dist/runtime/shared/block-picker.js +31 -0
  368. package/dist/runtime/shared/block-signature.d.ts +4 -0
  369. package/dist/runtime/shared/block-signature.js +46 -0
  370. package/dist/runtime/shared/clipboard.d.ts +31 -0
  371. package/dist/runtime/shared/clipboard.js +206 -0
  372. package/dist/runtime/shared/conditions.d.ts +36 -0
  373. package/dist/runtime/shared/conditions.js +100 -0
  374. package/dist/runtime/shared/content-routes.d.ts +42 -0
  375. package/dist/runtime/shared/content-routes.js +237 -0
  376. package/dist/runtime/shared/duplicate.js +2 -1
  377. package/dist/runtime/shared/editor-layout.d.ts +18 -0
  378. package/dist/runtime/shared/editor-layout.js +9 -0
  379. package/dist/runtime/shared/field-options.d.ts +21 -0
  380. package/dist/runtime/shared/field-options.js +90 -0
  381. package/dist/runtime/shared/graphql-sdl.d.ts +1 -0
  382. package/dist/runtime/shared/graphql-sdl.js +50 -26
  383. package/dist/runtime/shared/history.d.ts +27 -0
  384. package/dist/runtime/shared/history.js +123 -0
  385. package/dist/runtime/shared/hooks.d.ts +45 -0
  386. package/dist/runtime/shared/hooks.js +28 -0
  387. package/dist/runtime/shared/index.d.ts +90 -21
  388. package/dist/runtime/shared/index.js +121 -37
  389. package/dist/runtime/shared/inline-text.d.ts +38 -0
  390. package/dist/runtime/shared/inline-text.js +85 -0
  391. package/dist/runtime/shared/layout.d.ts +2 -1
  392. package/dist/runtime/shared/layout.js +3 -0
  393. package/dist/runtime/shared/links.d.ts +48 -0
  394. package/dist/runtime/shared/links.js +155 -0
  395. package/dist/runtime/shared/list-filters.d.ts +2 -0
  396. package/dist/runtime/shared/list-filters.js +2 -0
  397. package/dist/runtime/shared/media-focal.d.ts +30 -0
  398. package/dist/runtime/shared/media-focal.js +52 -0
  399. package/dist/runtime/shared/migrations-dir.d.ts +14 -0
  400. package/dist/runtime/shared/migrations-dir.js +48 -2
  401. package/dist/runtime/shared/presets.d.ts +67 -0
  402. package/dist/runtime/shared/presets.js +127 -0
  403. package/dist/runtime/shared/preview-links.d.ts +64 -0
  404. package/dist/runtime/shared/preview-links.js +54 -0
  405. package/dist/runtime/shared/preview.d.ts +124 -21
  406. package/dist/runtime/shared/preview.js +419 -81
  407. package/dist/runtime/shared/redirects.d.ts +75 -0
  408. package/dist/runtime/shared/redirects.js +230 -0
  409. package/dist/runtime/shared/roles.d.ts +57 -0
  410. package/dist/runtime/shared/roles.js +293 -0
  411. package/dist/runtime/shared/save-state.d.ts +8 -0
  412. package/dist/runtime/shared/save-state.js +15 -0
  413. package/dist/runtime/shared/search.d.ts +16 -1
  414. package/dist/runtime/shared/search.js +41 -7
  415. package/dist/runtime/shared/seo.d.ts +167 -0
  416. package/dist/runtime/shared/seo.js +335 -0
  417. package/dist/runtime/shared/transfer.js +2 -1
  418. package/dist/runtime/shared/translations.d.ts +29 -0
  419. package/dist/runtime/shared/translations.js +123 -0
  420. package/dist/runtime/shared/users.d.ts +1 -1
  421. package/dist/runtime/shared/validation.d.ts +23 -1
  422. package/dist/runtime/shared/validation.js +214 -64
  423. package/dist/runtime/webhooks.d.ts +33 -0
  424. package/dist/runtime/webhooks.js +57 -0
  425. package/dist/types.d.mts +5 -1
  426. package/package.json +6 -2
  427. package/dist/runtime/app/components/cms/BlockOutline.vue +0 -88
  428. package/dist/runtime/app/components/cms/BlockPalette.vue +0 -31
  429. package/dist/runtime/app/components/cms/SettingsModal.d.vue.ts +0 -12
  430. package/dist/runtime/app/components/cms/SettingsModal.vue +0 -126
  431. package/dist/runtime/app/components/cms/SettingsModal.vue.d.ts +0 -12
  432. package/dist/runtime/app/composables/cms-field-focus.d.ts +0 -4
  433. package/dist/runtime/app/composables/cms-field-focus.js +0 -20
@@ -6,8 +6,8 @@
6
6
  description="Set cms.media in nuxt.config to enable uploads."
7
7
  />
8
8
 
9
- <div v-else ref="gallery" class="cms-media-gallery" :class="{ 'is-compact': selectable }">
10
- <div class="cms-media-toolbar">
9
+ <KStack v-else ref="gallery" gap="md" :grow="fill" class="cms-media-gallery">
10
+ <KStack direction="row" align="center" gap="sm" wrap>
11
11
  <KInput
12
12
  v-model="search"
13
13
  :icon="cmsIcon('magnifying-glass')"
@@ -42,8 +42,10 @@
42
42
  label="New folder"
43
43
  :icon="cmsIcon('folder-plus')"
44
44
  variant="soft"
45
+ tone="neutral"
45
46
  size="sm"
46
47
  :disabled="!canNest"
48
+ :disabled-reason="`Folders go ${MEDIA_FOLDER_MAX_DEPTH} levels deep at most`"
47
49
  @click="openNewFolder"
48
50
  />
49
51
  <KButton
@@ -54,14 +56,15 @@
54
56
  />
55
57
  </template>
56
58
  </KStack>
57
- </div>
59
+ </KStack>
58
60
 
59
- <p v-if="sourceHint" class="cms-media-source">{{ sourceHint }}</p>
61
+ <KFieldMessage v-if="sourceHint" tone="neutral" :icon="false" :message="sourceHint" />
60
62
 
61
- <div class="cms-media-nav">
63
+ <KStack direction="row" justify="between" align="center" gap="sm" wrap>
62
64
  <KBreadcrumbs :items="crumbs" :separator="cmsIcon('chevron-right')" label="Folder" />
63
65
  <KStack v-if="folder && canManage" direction="row" gap="xs" align="center">
64
66
  <KButton
67
+ v-if="canMove"
65
68
  label="Rename"
66
69
  :icon="cmsIcon('pencil-square')"
67
70
  variant="ghost"
@@ -70,6 +73,7 @@
70
73
  @click="openRename"
71
74
  />
72
75
  <KButton
76
+ v-if="canDeleteMedia"
73
77
  label="Delete folder"
74
78
  :icon="cmsIcon('trash')"
75
79
  variant="ghost"
@@ -78,13 +82,16 @@
78
82
  @click="removeFolder(folder)"
79
83
  />
80
84
  </KStack>
81
- </div>
85
+ </KStack>
82
86
 
83
- <div v-if="uploader.uploading.value" class="cms-media-progress" role="status">
84
- <KSpinner size="xs" decorative />
85
- Uploading {{ Math.min(uploader.done.value + 1, uploader.total.value) }} of
86
- {{ uploader.total.value }} to {{ folder ?? "Library root" }}…
87
- </div>
87
+ <KProgress
88
+ v-if="uploader.uploading.value"
89
+ :value="uploader.done.value"
90
+ :max="uploader.total.value"
91
+ :label="uploadLabel"
92
+ show-label
93
+ show-value
94
+ />
88
95
 
89
96
  <KSelectionBar
90
97
  :count="selection.length"
@@ -97,13 +104,16 @@
97
104
  >
98
105
  <template #actions>
99
106
  <KButton
107
+ v-if="canMove"
100
108
  label="Move to…"
101
109
  :icon="cmsIcon('folder-arrow-down')"
102
110
  variant="soft"
111
+ tone="neutral"
103
112
  size="xs"
104
113
  @click="openMove"
105
114
  />
106
115
  <KButton
116
+ v-if="canDeleteMedia"
107
117
  label="Delete"
108
118
  :icon="cmsIcon('trash')"
109
119
  variant="soft"
@@ -114,9 +124,12 @@
114
124
  </template>
115
125
  </KSelectionBar>
116
126
 
117
- <div
127
+ <KStack
118
128
  v-if="!selection.length && (showTypeFilters || query && folder)"
119
- class="cms-media-filter-row"
129
+ direction="row"
130
+ align="center"
131
+ gap="sm"
132
+ wrap
120
133
  >
121
134
  <KFilterPills
122
135
  v-if="showTypeFilters"
@@ -131,11 +144,9 @@
131
144
  :options="scopeOptions"
132
145
  label="Search scope"
133
146
  />
134
- </div>
147
+ </KStack>
135
148
 
136
- <div v-if="library.loading.value" class="cms-media-loading">
137
- <KSpinner />
138
- </div>
149
+ <KSkeletonList v-if="library.loading.value" :rows="6" avatar class="cms-media-loading" />
139
150
 
140
151
  <KEmptyState
141
152
  v-else-if="loadError"
@@ -147,281 +158,207 @@
147
158
  label="Retry"
148
159
  :icon="cmsIcon('arrow-path')"
149
160
  variant="soft"
161
+ tone="neutral"
150
162
  @click="library.reload()"
151
163
  />
152
164
  </template>
153
165
  </KEmptyState>
154
166
 
155
167
  <template v-else>
156
- <section v-if="recent.length" class="cms-media-section">
157
- <h3 class="cms-media-section-title">
158
- <KIcon :name="cmsIcon('clock')" />
159
- Recent
160
- </h3>
161
- <div class="cms-media-grid is-compact is-recent">
162
- <KCard
163
- v-for="item in recent"
164
- :key="item.key"
165
- as="button"
166
- type="button"
167
- variant="outline"
168
- padding="none"
169
- :animated="false"
170
- class="cms-media-tile is-selectable"
171
- :title="mediaFilename(item.key)"
172
- :aria-label="mediaFilename(item.key)"
173
- @click="emitSelect(item)"
174
- >
175
- <div class="cms-media-preview">
176
- <img
177
- v-if="item.type === 'image' && item.url"
178
- :src="item.url"
179
- alt=""
180
- loading="lazy"
181
- />
182
- <video
183
- v-else-if="item.type === 'video' && item.url"
184
- :src="item.url"
185
- preload="metadata"
186
- muted
187
- playsinline
188
- />
189
- <KIcon v-else :name="cmsIcon(mediaIconFor(item.type))" />
190
- </div>
191
- </KCard>
192
- </div>
193
- </section>
168
+ <KStack v-if="recent.length" as="section" gap="sm">
169
+ <KSeparator variant="heading" label="Recent" :level="3" :lines="false" />
170
+ <KGrid :columns="6" min-item-width="4.5rem" gap="sm" class="cms-media-grid is-recent">
171
+ <KTooltip v-for="item in recent" :key="item.key" :text="mediaFilename(item.key)">
172
+ <KCard
173
+ as="button"
174
+ type="button"
175
+ variant="outline"
176
+ padding="none"
177
+ :animated="false"
178
+ class="cms-media-tile"
179
+ :aria-label="mediaFilename(item.key)"
180
+ @click="emitSelect(item)"
181
+ >
182
+ <KThumbnail :src="item.url ?? void 0" :kind="item.type" rounded="none" />
183
+ </KCard>
184
+ </KTooltip>
185
+ </KGrid>
186
+ </KStack>
194
187
 
195
- <section v-if="visibleFolders.length" class="cms-media-section">
196
- <h3 v-if="query" class="cms-media-section-title">Folders</h3>
197
- <div class="cms-media-folders">
198
- <button
199
- v-for="child in visibleFolders"
200
- :key="child"
201
- type="button"
202
- class="cms-media-folder-tile"
203
- :class="{ 'is-drop': dropTarget === targetId(child) }"
204
- :draggable="canManage"
205
- :title="child"
206
- @click="openFolder(child)"
207
- @dragstart="onFolderDragStart($event, child)"
208
- @dragend="onDragEnd"
209
- @dragover="onTargetDragOver($event, child)"
210
- @dragleave="onTargetDragLeave(child)"
211
- @drop="onTargetDrop($event, child)"
212
- >
213
- <span class="cms-media-folder-tile-icon">
214
- <KIcon
215
- :name="cmsIcon(dropTarget === targetId(child) ? 'folder-open' : 'folder')"
216
- />
217
- </span>
218
- <span class="cms-media-folder-tile-text">
219
- <span class="cms-media-folder-tile-name">
220
- {{ query ? child : mediaFolderName(child) }}
221
- </span>
222
- <span class="cms-media-folder-tile-meta">{{ folderMeta(child) }}</span>
223
- </span>
224
- </button>
225
- </div>
226
- </section>
188
+ <KStack v-if="visibleFolders.length" as="section" gap="sm">
189
+ <KSeparator v-if="query" variant="heading" label="Folders" :level="3" :lines="false" />
190
+ <KGrid :columns="6" min-item-width="10rem" gap="sm">
191
+ <KTooltip v-for="child in visibleFolders" :key="child" :text="child">
192
+ <KCard
193
+ as="button"
194
+ type="button"
195
+ variant="outline"
196
+ padding="sm"
197
+ :icon="cmsIcon(dropTarget === targetId(child) ? 'folder-open' : 'folder')"
198
+ :title="query ? child : mediaFolderName(child)"
199
+ :description="folderMeta(child)"
200
+ :selected="dropTarget === targetId(child) || void 0"
201
+ :draggable="canMove"
202
+ @click="openFolder(child)"
203
+ @dragstart="onFolderDragStart($event, child)"
204
+ @dragend="onDragEnd"
205
+ @dragover="onTargetDragOver($event, child)"
206
+ @dragleave="onTargetDragLeave(child)"
207
+ @drop="onTargetDrop($event, child)"
208
+ />
209
+ </KTooltip>
210
+ </KGrid>
211
+ </KStack>
227
212
 
228
- <section v-if="visibleFiles.length" class="cms-media-section">
229
- <h3
230
- v-if="query || visibleFolders.length || recent.length"
231
- class="cms-media-section-title"
232
- >
233
- {{
234
- query ? `${visibleFiles.length} file${visibleFiles.length > 1 ? "s" : ""}` : "Files"
235
- }}
236
- </h3>
213
+ <KSeparator
214
+ v-if="visibleFiles.length && (query || visibleFolders.length || recent.length)"
215
+ variant="heading"
216
+ :label="
217
+ query ? `${visibleFiles.length} file${visibleFiles.length > 1 ? 's' : ''}` : 'Files'
218
+ "
219
+ :level="3"
220
+ :lines="false"
221
+ />
237
222
 
238
- <div
239
- v-if="view === 'grid'"
240
- class="cms-media-grid"
241
- :class="{ 'is-compact': selectable }"
223
+ <KCard
224
+ v-if="visibleFiles.length || !visibleFolders.length"
225
+ :fill="fill"
226
+ :variant="fill ? 'raised' : 'outline'"
227
+ :padding="view === 'list' ? 'none' : 'sm'"
228
+ :animated="false"
229
+ :class="{ 'cms-media-files-fill': fill }"
230
+ >
231
+ <KTileGrid
232
+ v-if="visibleFiles.length && view === 'grid'"
233
+ :selected="selection"
234
+ :items="paged"
235
+ item-key="key"
236
+ :item-label="(item2) => mediaFilename(item2.key)"
237
+ label="Files"
238
+ :columns="selectable ? 3 : 5"
239
+ min-item-width="9rem"
240
+ :selectable="canManage"
241
+ :drag-data="canMove ? mediaDragData : void 0"
242
+ @update:selected="setSelection"
243
+ @activate="(item2) => openItem(item2)"
244
+ @item-drag-start="({ items }) => startItemDrag(items)"
245
+ @item-drag-end="onDragEnd"
242
246
  >
243
- <KCard
244
- v-for="item in paged"
245
- :key="item.key"
246
- variant="outline"
247
- padding="none"
248
- :animated="false"
249
- :selected="canManage ? isSelected(item.key) : void 0"
250
- role="button"
251
- tabindex="0"
252
- class="cms-media-tile is-selectable"
253
- :class="{ 'is-dragging': draggingKeys.includes(item.key) }"
254
- :draggable="canManage"
255
- :aria-pressed="canManage ? isSelected(item.key) : void 0"
256
- @click="onItemClick(item, $event)"
257
- @keydown.enter.prevent="onItemClick(item, $event)"
258
- @keydown.space.prevent="
259
- canManage ? toggleSelect(item.key, $event) : onItemClick(item, $event)
260
- "
261
- @dragstart="onItemDragStart($event, item)"
262
- @dragend="onDragEnd"
263
- >
264
- <div class="cms-media-preview">
265
- <img
266
- v-if="item.type === 'image' && item.url"
267
- :src="item.url"
268
- alt=""
269
- loading="lazy"
270
- draggable="false"
247
+ <template #tile="{ item }">
248
+ <KCard variant="outline" padding="none" :animated="false" class="cms-media-tile">
249
+ <KThumbnail
250
+ :src="item.url ?? void 0"
251
+ :kind="item.type"
252
+ rounded="none"
253
+ :caption="tileInfo(item)"
271
254
  />
272
- <video
273
- v-else-if="item.type === 'video' && item.url"
274
- :src="item.url"
275
- preload="metadata"
276
- muted
277
- playsinline
278
- />
279
- <KIcon v-else :name="cmsIcon(mediaIconFor(item.type))" />
280
- <button
281
- v-if="canManage"
282
- type="button"
283
- class="cms-media-check"
284
- :class="{ 'is-on': isSelected(item.key), 'is-visible': selection.length }"
285
- :aria-label="isSelected(item.key) ? 'Deselect' : 'Select'"
286
- tabindex="-1"
287
- @click.stop="toggleSelect(item.key, $event)"
288
- >
289
- <KIcon :name="cmsIcon('check')" />
290
- </button>
291
- <div class="cms-media-info">
292
- {{ tileInfo(item) }}
293
- </div>
294
- </div>
295
- <div class="cms-media-meta">
296
- <div class="cms-media-name" :title="item.key">
297
- {{ mediaFilename(item.key) }}
255
+ <div class="cms-media-meta">
256
+ <KTooltip :text="item.key">
257
+ <div class="cms-media-name">{{ mediaFilename(item.key) }}</div>
258
+ </KTooltip>
259
+ <div v-if="query && item.folder" class="cms-icon-hint">
260
+ <KIcon :name="cmsIcon('folder')" />{{ item.folder }}
261
+ </div>
262
+ <KStack v-if="!selectable" direction="row">
263
+ <KBadge
264
+ v-if="usageCount(item.key) !== null"
265
+ v-bind="usageBadge(item.key)"
266
+ size="sm"
267
+ />
268
+ <KSkeleton v-else width="5rem" />
269
+ </KStack>
298
270
  </div>
299
- <div v-if="query && item.folder" class="cms-media-folder">
300
- <KIcon :name="cmsIcon('folder')" />{{ item.folder }}
301
- </div>
302
- <span
303
- v-if="!selectable"
304
- class="cms-media-usage-badge"
305
- :class="{ 'is-unused': usageCount(item.key) === 0 }"
306
- >
307
- <template v-if="usageCount(item.key) !== null">
308
- <KIcon :name="cmsIcon(usageCount(item.key) ? 'link' : 'link-slash')" />{{
309
- usageLabel(item.key)
310
- }}
311
- </template>
312
- <template v-else>&nbsp;</template>
313
- </span>
314
- </div>
315
- </KCard>
316
- </div>
271
+ </KCard>
272
+ </template>
273
+ </KTileGrid>
317
274
 
318
- <KCard v-else variant="outline" padding="none" :animated="false" class="cms-media-list">
319
- <div
320
- v-for="item in paged"
321
- :key="item.key"
322
- role="button"
323
- tabindex="0"
324
- class="cms-media-row"
325
- :class="{
326
- 'is-checked': isSelected(item.key),
327
- 'is-dragging': draggingKeys.includes(item.key)
328
- }"
329
- :draggable="canManage"
330
- @click="onItemClick(item, $event)"
331
- @keydown.enter.prevent="onItemClick(item, $event)"
332
- @keydown.space.prevent="
333
- canManage ? toggleSelect(item.key, $event) : onItemClick(item, $event)
275
+ <KTable
276
+ v-else-if="visibleFiles.length"
277
+ v-model:selected="selection"
278
+ :rows="paged"
279
+ :columns="listColumns"
280
+ row-key="key"
281
+ link-key="name"
282
+ :selectable="canManage"
283
+ :row-draggable="canMove"
284
+ :drag-data="mediaDragData"
285
+ :fill="fill"
286
+ label="Files"
287
+ @row-drag-start="({ rows }) => startItemDrag(rows)"
288
+ @row-drag-end="onDragEnd"
289
+ @row-click="
290
+ (item2, _index, event) => onItemClick(item2, event)
334
291
  "
335
- @dragstart="onItemDragStart($event, item)"
336
- @dragend="onDragEnd"
337
- >
338
- <button
339
- v-if="canManage"
340
- type="button"
341
- class="cms-media-check is-visible"
342
- :class="{ 'is-on': isSelected(item.key) }"
343
- :aria-label="isSelected(item.key) ? 'Deselect' : 'Select'"
344
- tabindex="-1"
345
- @click.stop="toggleSelect(item.key, $event)"
346
- >
347
- <KIcon :name="cmsIcon('check')" />
348
- </button>
349
- <CmsMediaThumb :value="item.key" />
350
- <span class="cms-media-row-name" :title="item.key">
351
- {{ mediaFilename(item.key) }}
352
- <span v-if="query && item.folder" class="cms-media-folder">
353
- <KIcon :name="cmsIcon('folder')" />{{ item.folder }}
354
- </span>
355
- </span>
356
- <span v-if="!selectable" class="cms-media-row-cell is-usage">
357
- <span
358
- v-if="usageCount(item.key) !== null"
359
- class="cms-media-usage-badge"
360
- :class="{ 'is-unused': usageCount(item.key) === 0 }"
361
- >
362
- <KIcon :name="cmsIcon(usageCount(item.key) ? 'link' : 'link-slash')" />{{
363
- usageLabel(item.key)
364
- }}
365
- </span>
366
- </span>
367
- <span class="cms-media-row-cell is-mono">{{ extension(item.key) }}</span>
368
- <span class="cms-media-row-cell">{{
369
- item.size != null ? formatFileSize(item.size) : ""
370
- }}</span>
371
- <span class="cms-media-row-cell is-wide">
372
- {{ item.width && item.height ? `${item.width}\xD7${item.height}` : "" }}
373
- </span>
374
- <span class="cms-media-row-cell is-wide">
375
- {{ item.createdAt ? new Date(item.createdAt).toLocaleDateString() : "" }}
292
+ >
293
+ <template #cell-thumb="{ row }">
294
+ <CmsMediaThumb :value="row.key" />
295
+ </template>
296
+ <template #cell-name="{ row }">
297
+ {{ mediaFilename(row.key) }}
298
+ <span v-if="query && row.folder" class="cms-icon-hint">
299
+ <KIcon :name="cmsIcon('folder')" />{{ row.folder }}
376
300
  </span>
377
- </div>
378
- </KCard>
379
- </section>
301
+ </template>
302
+ <template #cell-usage="{ row }">
303
+ <KBadge
304
+ v-if="usageCount(row.key) !== null"
305
+ v-bind="usageBadge(row.key)"
306
+ size="sm"
307
+ />
308
+ </template>
309
+ </KTable>
380
310
 
381
- <KEmptyState
382
- v-if="!visibleFiles.length && !visibleFolders.length"
383
- :icon="cmsIcon(emptyState.icon)"
384
- :title="emptyState.title"
385
- :description="emptyState.description"
386
- >
387
- <template v-if="query || canUpload && !hasTypedFiles" #actions>
388
- <KButton
389
- v-if="query"
390
- label="Clear search"
391
- :icon="cmsIcon('x-mark')"
392
- variant="soft"
393
- @click="search = ''"
394
- />
395
- <KButton
396
- v-else
397
- label="Upload"
398
- :icon="cmsIcon('cloud-arrow-up')"
399
- variant="soft"
400
- @click="uploadOpen = true"
311
+ <KEmptyState
312
+ v-else
313
+ variant="plain"
314
+ fill
315
+ :icon="cmsIcon(emptyState.icon)"
316
+ :title="emptyState.title"
317
+ :description="emptyState.description"
318
+ >
319
+ <template v-if="query || canUpload && !hasTypedFiles" #actions>
320
+ <KButton
321
+ v-if="query"
322
+ label="Clear search"
323
+ :icon="cmsIcon('x-mark')"
324
+ variant="soft"
325
+ tone="neutral"
326
+ size="sm"
327
+ @click="search = ''"
328
+ />
329
+ <KButton
330
+ v-else
331
+ label="Upload"
332
+ :icon="cmsIcon('cloud-arrow-up')"
333
+ size="sm"
334
+ @click="uploadOpen = true"
335
+ />
336
+ </template>
337
+ </KEmptyState>
338
+
339
+ <template v-if="visibleFiles.length > PAGE_SIZE" #footer>
340
+ <KPagination
341
+ v-model:page="page"
342
+ :total="visibleFiles.length"
343
+ :page-size="PAGE_SIZE"
344
+ size="sm"
401
345
  />
402
346
  </template>
403
- </KEmptyState>
404
-
405
- <KPagination
406
- v-if="visibleFiles.length > PAGE_SIZE"
407
- v-model:page="page"
408
- :total="visibleFiles.length"
409
- :page-size="PAGE_SIZE"
410
- :show-summary="false"
411
- class="cms-media-pagination"
412
- />
347
+ </KCard>
413
348
  </template>
414
349
 
415
- <div v-if="fileDragActive" class="cms-media-drop-overlay">
416
- <KIcon :name="cmsIcon('cloud-arrow-up')" class="cms-media-drop-icon" />
417
- <span
418
- >Drop to upload to <strong>{{ dropTargetLabel }}</strong></span
419
- >
420
- </div>
421
- </div>
350
+ <KFileDropOverlay :active="fileDragActive" :icon="cmsIcon('cloud-arrow-up')">
351
+ Drop to upload to <strong>{{ dropTargetLabel }}</strong>
352
+ </KFileDropOverlay>
353
+ </KStack>
422
354
 
423
355
  <template v-if="!notConfigured">
424
- <KModal v-if="canUpload" v-model:open="uploadOpen" title="Upload media">
356
+ <KModal
357
+ v-if="canUpload"
358
+ v-model:open="uploadOpen"
359
+ title="Upload media"
360
+ description="Tip: you can also drop files or folders anywhere on the library, or on a folder."
361
+ >
425
362
  <CmsMediaUpload
426
363
  :multiple="!selectable"
427
364
  :media-type="mediaType"
@@ -430,87 +367,79 @@
430
367
  :directories="!selectable"
431
368
  @uploaded="onUploaded"
432
369
  />
433
- <p class="cms-media-hint cms-media-upload-hint">
434
- Tip: you can also drop files or folders anywhere on the library, or on a folder.
435
- </p>
436
370
  </KModal>
437
371
 
438
- <KModal v-if="canUpload" v-model:open="newFolderOpen" title="New folder" size="sm">
372
+ <KFormModal
373
+ v-if="canUpload"
374
+ v-model:open="newFolderOpen"
375
+ title="New folder"
376
+ size="sm"
377
+ :form="newFolderForm"
378
+ :guard="false"
379
+ creating
380
+ :submit-icon="cmsIcon('folder-plus')"
381
+ >
439
382
  <KFormField
440
383
  label="Name"
384
+ name="name"
385
+ required
441
386
  :description="
442
387
  newFolderPath ? `Creates ${newFolderPath}/` : 'Letters, numbers and dashes.'
443
388
  "
444
389
  >
445
- <KInput
446
- v-model="folderName"
447
- autofocus
448
- placeholder="e.g. covers"
449
- @keydown.enter.prevent="createFolder"
450
- />
390
+ <KInput v-model="folderDraft.name" autofocus placeholder="e.g. covers" />
451
391
  </KFormField>
452
- <template #footer>
453
- <KButton label="Cancel" variant="ghost" tone="neutral" @click="newFolderOpen = false" />
454
- <KButton
455
- label="Create"
456
- :loading="busy"
457
- :disabled="!newFolderPath"
458
- @click="createFolder"
459
- />
460
- </template>
461
- </KModal>
392
+ </KFormModal>
462
393
 
463
- <KModal v-if="canManage" v-model:open="renameOpen" title="Rename folder" size="sm">
464
- <KStack gap="md">
465
- <KFormField label="Name">
466
- <KInput v-model="folderName" autofocus @keydown.enter.prevent="renameFolder" />
467
- </KFormField>
468
- <KFormField label="Location">
469
- <CmsMediaFolderPicker
470
- v-model="renameParent"
471
- :folders="parentCandidates"
472
- :exclude="folder"
473
- />
474
- </KFormField>
475
- <p class="cms-media-hint">
476
- {{
477
- renamePath ? `Moves ${folder}/ to ${renamePath}/` : "Letters, numbers and dashes."
478
- }}
479
- </p>
480
- </KStack>
481
- <template #footer>
482
- <KButton label="Cancel" variant="ghost" tone="neutral" @click="renameOpen = false" />
483
- <KButton
484
- label="Save"
485
- :loading="busy"
486
- :disabled="!renamePath || renamePath === folder"
487
- @click="renameFolder"
394
+ <KFormModal
395
+ v-if="canManage"
396
+ v-model:open="renameOpen"
397
+ title="Rename folder"
398
+ size="sm"
399
+ :form="renameForm"
400
+ :guard="false"
401
+ >
402
+ <KFormField
403
+ label="Name"
404
+ name="name"
405
+ required
406
+ :description="
407
+ renamePath ? `Moves ${folder}/ to ${renamePath}/` : 'Letters, numbers and dashes.'
408
+ "
409
+ >
410
+ <KInput v-model="folderDraft.name" autofocus />
411
+ </KFormField>
412
+ <KFormField label="Location" name="parent">
413
+ <CmsMediaFolderPicker
414
+ v-model="folderDraft.parent"
415
+ :folders="parentCandidates"
416
+ :exclude="folder"
488
417
  />
489
- </template>
490
- </KModal>
418
+ </KFormField>
419
+ </KFormModal>
491
420
 
492
- <KModal
421
+ <KFormModal
493
422
  v-if="canManage"
494
423
  v-model:open="moveOpen"
495
424
  :title="`Move ${selection.length} file${selection.length > 1 ? 's' : ''}`"
496
425
  size="sm"
426
+ :form="moveForm"
427
+ :guard="false"
428
+ :require-dirty="false"
429
+ submit-label="Move here"
497
430
  >
498
- <CmsMediaFolderPicker
499
- v-model="moveTarget"
500
- :folders="library.folders.value"
501
- label="Folder"
502
- />
503
- <template #footer>
504
- <KButton label="Cancel" variant="ghost" tone="neutral" @click="moveOpen = false" />
505
- <KButton label="Move here" :loading="busy" @click="moveSelection" />
506
- </template>
507
- </KModal>
431
+ <KFormField label="Folder" name="target">
432
+ <CmsMediaFolderPicker v-model="moveDraft.target" :folders="library.folders.value" />
433
+ </KFormField>
434
+ </KFormModal>
508
435
 
509
436
  <CmsMediaDetail
510
437
  v-if="!selectable"
511
438
  :item="detail"
512
439
  :library="library"
513
440
  :read-only="readOnly"
441
+ :can-edit="canWrite"
442
+ :can-delete="canDeleteMedia"
514
443
  @close="detailKey = null"
515
444
  @delete="(item2) => removeItems([item2])"
516
445
  />
@@ -520,22 +449,37 @@
520
449
  <script setup>
521
450
  import { computed, onMounted, ref, useTemplateRef, watch } from "#imports";
522
451
  import {
452
+ Badge as KBadge,
523
453
  Breadcrumbs as KBreadcrumbs,
524
454
  Button as KButton,
525
455
  Card as KCard,
526
456
  DropdownMenu as KDropdownMenu,
527
457
  EmptyState as KEmptyState,
458
+ FileDropOverlay as KFileDropOverlay,
459
+ FieldMessage as KFieldMessage,
528
460
  FilterPills as KFilterPills,
529
461
  FormField as KFormField,
462
+ FormModal as KFormModal,
463
+ Grid as KGrid,
530
464
  Icon as KIcon,
531
465
  Input as KInput,
532
466
  Modal as KModal,
533
467
  Pagination as KPagination,
468
+ Progress as KProgress,
534
469
  SegmentedControl as KSegmentedControl,
535
470
  SelectionBar as KSelectionBar,
536
- Spinner as KSpinner,
471
+ Separator as KSeparator,
472
+ Skeleton as KSkeleton,
473
+ SkeletonList as KSkeletonList,
537
474
  Stack as KStack,
538
- useFileDrop
475
+ Table as KTable,
476
+ Thumbnail as KThumbnail,
477
+ TileGrid as KTileGrid,
478
+ Tooltip as KTooltip,
479
+ toast,
480
+ useFileDrop,
481
+ useForm,
482
+ useRouteState
539
483
  } from "@xleddyl/443-components-vue";
540
484
  import {
541
485
  isWithinMediaFolder,
@@ -547,23 +491,24 @@ import {
547
491
  mediaFolderDepth,
548
492
  mediaFolderName,
549
493
  mediaFolderParent,
550
- mediaIconFor,
551
494
  groupMediaUsage,
552
495
  mediaUsageGroups,
496
+ otherEntriesLabel,
553
497
  mediaTypeAccept,
554
498
  mediaTypeFilter,
555
499
  rebaseMediaFolder,
556
500
  slugify
557
501
  } from "#nuxt-cms";
558
502
  import { useCmsConfirm } from "../../composables/cms-confirm";
559
- import { useCmsMediaLibrary, usageLink } from "../../composables/cms-media-library";
503
+ import { useCmsPermissions } from "../../composables/cms-permissions";
504
+ import { useCmsMediaLibrary, usageItem } from "../../composables/cms-media-library";
560
505
  import {
561
506
  uploadTreeFromDrop,
562
507
  uploadTreeFromFiles,
563
508
  useCmsMediaUploader
564
509
  } from "../../composables/cms-media-uploader";
510
+ import { CMS_MEDIA_ROUTE_KEY, useCmsOverlayFlag } from "../../composables/cms-route";
565
511
  import { useCmsRuntime } from "../../composables/cms-runtime";
566
- import { useCmsToast } from "../../composables/cms-toast";
567
512
  import { cmsIcon } from "../../utils/ui";
568
513
  const MEDIA_DRAG = "application/x-cms-media";
569
514
  const FOLDER_DRAG = "application/x-cms-folder";
@@ -592,19 +537,25 @@ const scopeOptions = [
592
537
  ];
593
538
  const props = defineProps({
594
539
  selectable: { type: Boolean, required: false },
540
+ fill: { type: Boolean, required: false },
595
541
  mediaType: { type: null, required: false },
596
542
  accept: { type: Array, required: false }
597
543
  });
598
544
  const emit = defineEmits(["select"]);
599
545
  const folder = defineModel("folder", { type: [String, null], ...{ default: null } });
600
- const toast = useCmsToast();
601
546
  const confirmAction = useCmsConfirm();
602
547
  const runtime = useCmsRuntime();
603
548
  const library = useCmsMediaLibrary();
604
549
  const uploader = useCmsMediaUploader();
550
+ const { can } = useCmsPermissions();
605
551
  const readOnly = computed(() => runtime.mediaStorage === "local");
606
- const canUpload = computed(() => !readOnly.value);
607
- const canManage = computed(() => !readOnly.value && !props.selectable);
552
+ const canWrite = computed(() => can("media.write"));
553
+ const canDeleteMedia = computed(() => can("media.delete"));
554
+ const canUpload = computed(() => !readOnly.value && canWrite.value);
555
+ const canManage = computed(
556
+ () => !readOnly.value && !props.selectable && (canWrite.value || canDeleteMedia.value)
557
+ );
558
+ const canMove = computed(() => canManage.value && canWrite.value);
608
559
  const canNest = computed(() => mediaFolderDepth(folder.value) < MEDIA_FOLDER_MAX_DEPTH);
609
560
  onMounted(() => library.reload());
610
561
  const notConfigured = computed(() => library.errorCode.value === 501);
@@ -719,7 +670,7 @@ watch(visibleFiles, (list) => {
719
670
  const paged = computed(
720
671
  () => visibleFiles.value.slice((page.value - 1) * PAGE_SIZE, page.value * PAGE_SIZE)
721
672
  );
722
- const usageByKey = ref({});
673
+ const usageReport = ref({ usage: {}, hidden: {} });
723
674
  let usageRequest = 0;
724
675
  watch(
725
676
  () => [props.selectable, library.loading.value, paged.value],
@@ -728,13 +679,16 @@ watch(
728
679
  if (selectable || loading || !items.length) return;
729
680
  const usage = await library.usage(items.map((item) => item.key));
730
681
  if (request !== usageRequest) return;
731
- usageByKey.value = usage ?? {};
682
+ usageReport.value = usage ?? { usage: {}, hidden: {} };
732
683
  },
733
684
  { immediate: true }
734
685
  );
735
686
  const usageCounts = computed(
736
687
  () => new Map(
737
- Object.entries(usageByKey.value).map(([key, list]) => [key, groupMediaUsage(list).length])
688
+ Object.entries(usageReport.value.usage).map(([key, list]) => [
689
+ key,
690
+ groupMediaUsage(list).length + (usageReport.value.hidden[key] ?? 0)
691
+ ])
738
692
  )
739
693
  );
740
694
  function usageCount(key) {
@@ -848,6 +802,9 @@ function toggleSelect(key, event) {
848
802
  selection.value = isSelected(key) ? selection.value.filter((entry) => entry !== key) : [...selection.value, key];
849
803
  lastSelected.value = key;
850
804
  }
805
+ function setSelection(keys) {
806
+ selection.value = keys.map(String);
807
+ }
851
808
  function selectAll() {
852
809
  selection.value = visibleFiles.value.map((item) => item.key);
853
810
  }
@@ -855,13 +812,23 @@ function clearSelection() {
855
812
  selection.value = [];
856
813
  lastSelected.value = null;
857
814
  }
858
- const detailKey = ref(null);
815
+ const detailRoute = useRouteState(CMS_MEDIA_ROUTE_KEY, { location: "hash" });
816
+ const detailKey = computed({
817
+ get: () => props.selectable ? null : detailRoute.value ?? null,
818
+ set: (value) => {
819
+ if (!props.selectable) detailRoute.value = value ?? void 0;
820
+ }
821
+ });
859
822
  const detail = computed(
860
823
  () => library.items.value.find((item) => item.key === detailKey.value) ?? null
861
824
  );
862
825
  function emitSelect(item) {
863
826
  emit("select", { key: item.key, url: item.url });
864
827
  }
828
+ function openItem(item) {
829
+ if (props.selectable) emitSelect(item);
830
+ else detailKey.value = item.key;
831
+ }
865
832
  function onItemClick(item, event) {
866
833
  if (props.selectable) {
867
834
  emitSelect(item);
@@ -883,31 +850,36 @@ async function run(action) {
883
850
  busy.value = false;
884
851
  }
885
852
  }
886
- const newFolderOpen = ref(false);
887
- const folderName = ref("");
888
- const folderSlug = computed(() => slugify(folderName.value));
853
+ const newFolderOpen = useCmsOverlayFlag("newfolder", !props.selectable);
854
+ const folderDraft = ref({ name: "", parent: null });
855
+ const folderSlug = computed(() => slugify(folderDraft.value.name));
889
856
  const newFolderPath = computed(
890
857
  () => folderSlug.value ? folder.value ? `${folder.value}/${folderSlug.value}` : folderSlug.value : null
891
858
  );
892
859
  function openNewFolder() {
893
- folderName.value = "";
894
860
  newFolderOpen.value = true;
895
861
  }
896
- async function createFolder() {
897
- const path = newFolderPath.value;
898
- if (!path) return;
899
- if (library.folders.value.includes(path)) {
862
+ watch(newFolderOpen, (open) => {
863
+ if (open) newFolderForm.reset({ name: "", parent: null });
864
+ });
865
+ const folderNameRule = {
866
+ required: true,
867
+ maxLength: 64,
868
+ validate: (value) => slugify(value) ? void 0 : "Use letters, numbers or dashes"
869
+ };
870
+ const newFolderForm = useForm({
871
+ values: folderDraft,
872
+ schema: { name: folderNameRule },
873
+ guard: false,
874
+ onSubmit: async () => {
875
+ const path = newFolderPath.value;
876
+ if (!path) return;
877
+ if (!library.folders.value.includes(path) && !await library.createFolder(path)) return;
900
878
  newFolderOpen.value = false;
901
879
  openFolder(path);
902
- return;
903
880
  }
904
- const ok = await run(() => library.createFolder(path));
905
- if (!ok) return;
906
- newFolderOpen.value = false;
907
- openFolder(path);
908
- }
909
- const renameOpen = ref(false);
910
- const renameParent = ref(null);
881
+ });
882
+ const renameOpen = useCmsOverlayFlag("renamefolder", !props.selectable);
911
883
  const parentCandidates = computed(() => {
912
884
  const own = folder.value;
913
885
  if (!own) return [];
@@ -920,47 +892,58 @@ const parentCandidates = computed(() => {
920
892
  });
921
893
  const renamePath = computed(() => {
922
894
  if (!folderSlug.value) return null;
923
- return renameParent.value ? `${renameParent.value}/${folderSlug.value}` : folderSlug.value;
895
+ const parent = folderDraft.value.parent;
896
+ return parent ? `${parent}/${folderSlug.value}` : folderSlug.value;
924
897
  });
925
898
  function openRename() {
926
- if (!folder.value) return;
927
- folderName.value = mediaFolderName(folder.value);
928
- renameParent.value = mediaFolderParent(folder.value);
929
- renameOpen.value = true;
899
+ if (folder.value) renameOpen.value = true;
930
900
  }
931
- async function renameFolder() {
932
- const from = folder.value;
933
- const to = renamePath.value;
934
- if (!from || !to || from === to) return;
935
- const ok = await run(() => library.moveFolder(from, to));
936
- if (!ok) return;
937
- renameOpen.value = false;
938
- openFolder(to);
939
- }
940
- async function confirmDelete(keys, question, title) {
901
+ watch(renameOpen, (open) => {
902
+ if (!open) return;
903
+ if (!folder.value) {
904
+ renameOpen.value = false;
905
+ return;
906
+ }
907
+ renameForm.reset({
908
+ name: mediaFolderName(folder.value),
909
+ parent: mediaFolderParent(folder.value)
910
+ });
911
+ });
912
+ const renameForm = useForm({
913
+ values: folderDraft,
914
+ schema: { name: folderNameRule },
915
+ guard: false,
916
+ onSubmit: async () => {
917
+ const from = folder.value;
918
+ const to = renamePath.value;
919
+ if (!from || !to || from === to) {
920
+ renameOpen.value = false;
921
+ return;
922
+ }
923
+ if (!await library.moveFolder(from, to)) return;
924
+ renameOpen.value = false;
925
+ openFolder(to);
926
+ }
927
+ });
928
+ async function confirmDelete(keys, question) {
941
929
  const usage = await library.usage(keys);
942
- const groups = usage ? mediaUsageGroups(usage, keys) : [];
930
+ const groups = usage ? mediaUsageGroups(usage.usage, keys, usage.hidden) : [];
943
931
  if (!groups.length) {
944
932
  const warning = usage ? "" : " Could not check where the files are used.";
945
- return confirmAction(`${question}${warning}`, { title, confirmLabel: "Delete" });
933
+ return confirmAction({ title: question, description: warning.trim() || void 0 });
946
934
  }
947
935
  const single = keys.length === 1;
948
- const usedIn = single ? `This file is used in ${entriesLabel(groups[0].entries.length)}.` : `${groups.length} of ${keys.length} files are used in your content.`;
949
- return confirmAction(
950
- `${question} ${usedIn} Deleting ${single ? "it" : "them"} leaves broken links there.`,
951
- {
952
- title: single ? "File in use" : "Files in use",
953
- confirmLabel: "Delete anyway",
954
- groups: groups.map((group) => ({
955
- title: group.key,
956
- links: group.entries.map((entry) => ({
957
- title: entry.title ?? entry.label,
958
- meta: `${entry.label} \xB7 ${entry.fields.join(", ")}`,
959
- to: usageLink(entry)
960
- }))
961
- }))
962
- }
963
- );
936
+ const usedIn = single ? `This file is used in ${entriesLabel(groups[0].entries.length + groups[0].hidden)}.` : `${groups.length} of ${keys.length} files are used in your content.`;
937
+ return confirmAction({
938
+ title: single ? "File in use" : "Files in use",
939
+ description: `${question} ${usedIn} Deleting ${single ? "it" : "them"} leaves broken links there.`,
940
+ confirmLabel: "Delete anyway",
941
+ groups: groups.map((group) => ({
942
+ title: group.key,
943
+ items: group.entries.map(usageItem),
944
+ note: group.hidden ? otherEntriesLabel(group.hidden) : void 0
945
+ }))
946
+ });
964
947
  }
965
948
  async function removeFolder(path) {
966
949
  const files = library.itemsWithin(path);
@@ -973,8 +956,7 @@ async function removeFolder(path) {
973
956
  const question = contents.length ? `Delete "${mediaFolderName(path)}" with ${contents.join(" and ")}?` : `Delete the empty folder "${mediaFolderName(path)}"?`;
974
957
  if (!await confirmDelete(
975
958
  files.map((item) => item.key),
976
- question,
977
- "Delete folder"
959
+ question
978
960
  ))
979
961
  return;
980
962
  const ok = await run(() => library.deleteFolder(path));
@@ -986,18 +968,34 @@ async function removeItems(items) {
986
968
  if (!items.length) return;
987
969
  const keys = items.map((item) => item.key);
988
970
  const question = keys.length === 1 ? `Delete "${mediaFilename(keys[0])}"?` : `Delete ${keys.length} files?`;
989
- if (!await confirmDelete(keys, question, "Delete")) return;
971
+ if (!await confirmDelete(keys, question)) return;
990
972
  const ok = await run(() => library.deleteItems(keys));
991
973
  if (!ok) return;
992
974
  if (detailKey.value && keys.includes(detailKey.value)) detailKey.value = null;
993
975
  clearSelection();
994
976
  }
995
- const moveOpen = ref(false);
996
- const moveTarget = ref(null);
977
+ const moveOpen = useCmsOverlayFlag("move", !props.selectable);
978
+ const moveDraft = ref({ target: null });
997
979
  function openMove() {
998
- moveTarget.value = folder.value;
999
980
  moveOpen.value = true;
1000
981
  }
982
+ watch(moveOpen, (open) => {
983
+ if (!open) return;
984
+ if (!selection.value.length) {
985
+ moveOpen.value = false;
986
+ return;
987
+ }
988
+ moveForm.reset({ target: folder.value });
989
+ });
990
+ const moveForm = useForm({
991
+ values: moveDraft,
992
+ guard: false,
993
+ onSubmit: async (values) => {
994
+ if (!await moveKeys(selection.value, values.target)) return;
995
+ moveOpen.value = false;
996
+ clearSelection();
997
+ }
998
+ });
1001
999
  async function moveKeys(keys, target) {
1002
1000
  const moving = keys.filter(
1003
1001
  (key) => (library.items.value.find((item) => item.key === key)?.folder ?? null) !== target
@@ -1005,20 +1003,13 @@ async function moveKeys(keys, target) {
1005
1003
  if (!moving.length) return true;
1006
1004
  const ok = await run(() => library.moveItems(moving, target));
1007
1005
  if (ok) {
1008
- toast.add({
1009
- title: `Moved ${moving.length} file${moving.length > 1 ? "s" : ""} to ${target ?? "Library root"}`,
1010
- color: "success"
1011
- });
1006
+ toast.success(
1007
+ `Moved ${moving.length} file${moving.length > 1 ? "s" : ""} to ${target ?? "Library root"}`
1008
+ );
1012
1009
  }
1013
1010
  return ok;
1014
1011
  }
1015
- async function moveSelection() {
1016
- const ok = await moveKeys(selection.value, moveTarget.value);
1017
- if (!ok) return;
1018
- moveOpen.value = false;
1019
- clearSelection();
1020
- }
1021
- const uploadOpen = ref(false);
1012
+ const uploadOpen = useCmsOverlayFlag("upload", !props.selectable);
1022
1013
  async function afterUpload(uploaded) {
1023
1014
  if (!uploaded.length) {
1024
1015
  if (!props.selectable) await library.reload({ quiet: true });
@@ -1029,10 +1020,7 @@ async function afterUpload(uploaded) {
1029
1020
  return;
1030
1021
  }
1031
1022
  await library.reload({ quiet: true });
1032
- toast.add({
1033
- title: `Uploaded ${uploaded.length} file${uploaded.length > 1 ? "s" : ""}`,
1034
- color: "success"
1035
- });
1023
+ toast.success(`Uploaded ${uploaded.length} file${uploaded.length > 1 ? "s" : ""}`);
1036
1024
  }
1037
1025
  function onUploaded(uploaded) {
1038
1026
  uploadOpen.value = false;
@@ -1055,8 +1043,8 @@ async function uploadTree(pending, target) {
1055
1043
  const draggingKeys = ref([]);
1056
1044
  const draggingFolder = ref(null);
1057
1045
  const dropTarget = ref(null);
1058
- const galleryEl = useTemplateRef("gallery");
1059
- const { over: fileDragOver } = useFileDrop(galleryEl, {
1046
+ const galleryRef = useTemplateRef("gallery");
1047
+ const { over: fileDragOver } = useFileDrop(() => galleryRef.value?.$el, {
1060
1048
  disabled: () => !canUpload.value,
1061
1049
  ignore: (event) => event.defaultPrevented,
1062
1050
  onDrop: (_files, event) => {
@@ -1077,15 +1065,14 @@ const dropTargetLabel = computed(() => {
1077
1065
  function hasFiles(event) {
1078
1066
  return !!event.dataTransfer?.types.includes("Files");
1079
1067
  }
1080
- function onItemDragStart(event, item) {
1081
- if (!canManage.value || !event.dataTransfer) return;
1082
- const keys = isSelected(item.key) ? [...selection.value] : [item.key];
1083
- draggingKeys.value = keys;
1084
- event.dataTransfer.effectAllowed = "move";
1085
- event.dataTransfer.setData(MEDIA_DRAG, JSON.stringify(keys));
1068
+ function mediaDragData(_item, items) {
1069
+ return { [MEDIA_DRAG]: JSON.stringify(items.map((entry) => entry.key)) };
1070
+ }
1071
+ function startItemDrag(items) {
1072
+ draggingKeys.value = items.map((item) => item.key);
1086
1073
  }
1087
1074
  function onFolderDragStart(event, path) {
1088
- if (!canManage.value || !event.dataTransfer) return;
1075
+ if (!canMove.value || !event.dataTransfer) return;
1089
1076
  draggingFolder.value = path;
1090
1077
  event.dataTransfer.effectAllowed = "move";
1091
1078
  event.dataTransfer.setData(FOLDER_DRAG, path);
@@ -1143,6 +1130,42 @@ async function onTargetDrop(event, target) {
1143
1130
  }
1144
1131
  if (tree) await uploadTree(tree, target);
1145
1132
  }
1133
+ const uploadLabel = computed(
1134
+ () => `Uploading ${Math.min(uploader.done.value + 1, uploader.total.value)} of ${uploader.total.value} to ${folder.value ?? "Library root"}`
1135
+ );
1136
+ const listColumns = computed(() => [
1137
+ { key: "thumb", label: "Preview", hideLabel: true, width: "3rem" },
1138
+ { key: "name", label: "Name", strong: true },
1139
+ ...props.selectable ? [] : [{ key: "usage", label: "Usage" }],
1140
+ { key: "type", label: "Type", value: (item) => extension(item.key).toUpperCase() },
1141
+ {
1142
+ key: "size",
1143
+ label: "Size",
1144
+ numeric: true,
1145
+ value: (item) => item.size != null ? formatFileSize(item.size) : ""
1146
+ },
1147
+ {
1148
+ key: "dimensions",
1149
+ label: "Dimensions",
1150
+ numeric: true,
1151
+ value: (item) => item.width && item.height ? `${item.width}\xD7${item.height}` : ""
1152
+ },
1153
+ {
1154
+ key: "created",
1155
+ label: "Added",
1156
+ numeric: true,
1157
+ value: (item) => item.createdAt ? new Date(item.createdAt).toLocaleDateString() : ""
1158
+ }
1159
+ ]);
1160
+ function usageBadge(key) {
1161
+ const count = usageCount(key);
1162
+ return {
1163
+ label: usageLabel(key),
1164
+ icon: cmsIcon(count ? "link" : "link-slash"),
1165
+ tone: count ? "accent" : "neutral",
1166
+ variant: count ? "soft" : "outline"
1167
+ };
1168
+ }
1146
1169
  function tileInfo(item) {
1147
1170
  const parts = [extension(item.key)];
1148
1171
  if (item.size != null) parts.push(formatFileSize(item.size));