@xleddyl/nuxt-cms 0.9.4 → 0.10.1

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 (431) 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 -219
  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 -147
  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 +19 -11
  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 +40 -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 +8 -8
  124. package/dist/runtime/app/components/cms/TranslationBadges.d.vue.ts +7 -0
  125. package/dist/runtime/app/components/cms/TranslationBadges.vue +44 -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 +1153 -338
  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 +191 -54
  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 +27 -9
  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 +143 -28
  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/site.d.ts +23 -0
  351. package/dist/runtime/server/utils/site.js +79 -0
  352. package/dist/runtime/server/utils/sitemap.d.ts +12 -0
  353. package/dist/runtime/server/utils/sitemap.js +109 -0
  354. package/dist/runtime/server/utils/user-management.d.ts +4 -0
  355. package/dist/runtime/server/utils/user-management.js +20 -0
  356. package/dist/runtime/server/utils/users.d.ts +1 -1
  357. package/dist/runtime/server/utils/users.js +1 -1
  358. package/dist/runtime/server/utils/visibility.d.ts +1 -0
  359. package/dist/runtime/server/utils/visibility.js +6 -1
  360. package/dist/runtime/server/utils/webhooks.d.ts +24 -0
  361. package/dist/runtime/server/utils/webhooks.js +90 -0
  362. package/dist/runtime/shared/block-paths.d.ts +66 -0
  363. package/dist/runtime/shared/block-paths.js +310 -0
  364. package/dist/runtime/shared/block-picker.d.ts +18 -0
  365. package/dist/runtime/shared/block-picker.js +31 -0
  366. package/dist/runtime/shared/block-signature.d.ts +4 -0
  367. package/dist/runtime/shared/block-signature.js +46 -0
  368. package/dist/runtime/shared/clipboard.d.ts +31 -0
  369. package/dist/runtime/shared/clipboard.js +206 -0
  370. package/dist/runtime/shared/conditions.d.ts +36 -0
  371. package/dist/runtime/shared/conditions.js +100 -0
  372. package/dist/runtime/shared/content-routes.d.ts +42 -0
  373. package/dist/runtime/shared/content-routes.js +237 -0
  374. package/dist/runtime/shared/duplicate.js +2 -1
  375. package/dist/runtime/shared/editor-layout.d.ts +18 -0
  376. package/dist/runtime/shared/editor-layout.js +9 -0
  377. package/dist/runtime/shared/field-options.d.ts +21 -0
  378. package/dist/runtime/shared/field-options.js +90 -0
  379. package/dist/runtime/shared/graphql-sdl.d.ts +1 -0
  380. package/dist/runtime/shared/graphql-sdl.js +50 -26
  381. package/dist/runtime/shared/history.d.ts +27 -0
  382. package/dist/runtime/shared/history.js +123 -0
  383. package/dist/runtime/shared/hooks.d.ts +45 -0
  384. package/dist/runtime/shared/hooks.js +28 -0
  385. package/dist/runtime/shared/index.d.ts +90 -21
  386. package/dist/runtime/shared/index.js +121 -37
  387. package/dist/runtime/shared/inline-text.d.ts +38 -0
  388. package/dist/runtime/shared/inline-text.js +85 -0
  389. package/dist/runtime/shared/layout.d.ts +2 -1
  390. package/dist/runtime/shared/layout.js +3 -0
  391. package/dist/runtime/shared/links.d.ts +48 -0
  392. package/dist/runtime/shared/links.js +155 -0
  393. package/dist/runtime/shared/list-filters.d.ts +2 -0
  394. package/dist/runtime/shared/list-filters.js +2 -0
  395. package/dist/runtime/shared/media-focal.d.ts +30 -0
  396. package/dist/runtime/shared/media-focal.js +52 -0
  397. package/dist/runtime/shared/migrations-dir.d.ts +14 -0
  398. package/dist/runtime/shared/migrations-dir.js +48 -2
  399. package/dist/runtime/shared/presets.d.ts +67 -0
  400. package/dist/runtime/shared/presets.js +127 -0
  401. package/dist/runtime/shared/preview-links.d.ts +64 -0
  402. package/dist/runtime/shared/preview-links.js +54 -0
  403. package/dist/runtime/shared/preview.d.ts +124 -21
  404. package/dist/runtime/shared/preview.js +419 -81
  405. package/dist/runtime/shared/redirects.d.ts +75 -0
  406. package/dist/runtime/shared/redirects.js +230 -0
  407. package/dist/runtime/shared/roles.d.ts +57 -0
  408. package/dist/runtime/shared/roles.js +293 -0
  409. package/dist/runtime/shared/save-state.d.ts +8 -0
  410. package/dist/runtime/shared/save-state.js +15 -0
  411. package/dist/runtime/shared/search.d.ts +16 -1
  412. package/dist/runtime/shared/search.js +41 -7
  413. package/dist/runtime/shared/seo.d.ts +167 -0
  414. package/dist/runtime/shared/seo.js +335 -0
  415. package/dist/runtime/shared/transfer.js +2 -1
  416. package/dist/runtime/shared/translations.d.ts +29 -0
  417. package/dist/runtime/shared/translations.js +123 -0
  418. package/dist/runtime/shared/users.d.ts +1 -1
  419. package/dist/runtime/shared/validation.d.ts +23 -1
  420. package/dist/runtime/shared/validation.js +214 -64
  421. package/dist/runtime/webhooks.d.ts +33 -0
  422. package/dist/runtime/webhooks.js +57 -0
  423. package/dist/types.d.mts +5 -1
  424. package/package.json +6 -2
  425. package/dist/runtime/app/components/cms/BlockOutline.vue +0 -88
  426. package/dist/runtime/app/components/cms/BlockPalette.vue +0 -31
  427. package/dist/runtime/app/components/cms/SettingsModal.d.vue.ts +0 -12
  428. package/dist/runtime/app/components/cms/SettingsModal.vue +0 -126
  429. package/dist/runtime/app/components/cms/SettingsModal.vue.d.ts +0 -12
  430. package/dist/runtime/app/composables/cms-field-focus.d.ts +0 -4
  431. package/dist/runtime/app/composables/cms-field-focus.js +0 -20
@@ -1,35 +1,159 @@
1
1
  <template>
2
- <div class="cms-editor">
3
- <header class="cms-editor-bar">
4
- <div class="cms-editor-bar-start">
5
- <slot name="start" />
6
- <div class="cms-editor-heading">
7
- <span class="cms-editor-title" :title="title">{{ title }}</span>
8
- <slot name="status" />
9
- </div>
2
+ <KWorkspaceLayout
3
+ ref="workspace"
4
+ v-model:pane="pane"
5
+ v-model:right-collapsed="inspectorCollapsed"
6
+ storage-key="visual-editor"
7
+ :left="leftPane"
8
+ :main="{ label: 'Preview' }"
9
+ :right="rightPane"
10
+ panes-label="Editor panels"
11
+ >
12
+ <template #start="{ compact }">
13
+ <slot name="start" />
14
+ <KPageHeader
15
+ size="sm"
16
+ :title="title"
17
+ :trail="compact ? void 0 : trail"
18
+ :level="1"
19
+ truncate
20
+ >
21
+ <template v-if="$slots.status" #badge><slot name="status" /></template>
22
+ </KPageHeader>
23
+ </template>
24
+
25
+ <template #center="{ compact }">
26
+ <CmsLocaleSwitch
27
+ v-if="i18n.locales.length > 1"
28
+ v-model="locale"
29
+ :progress="translations"
30
+ />
31
+ <KSegmentedControl
32
+ v-model="device"
33
+ :options="deviceOptions"
34
+ size="sm"
35
+ icon-only
36
+ required
37
+ aria-label="Preview width"
38
+ />
39
+ <CmsEditorHistory
40
+ v-if="!readonly && compact"
41
+ :can-undo="canUndo"
42
+ :can-redo="canRedo"
43
+ @undo="undo"
44
+ @redo="redo"
45
+ />
46
+ </template>
47
+
48
+ <template #end="{ compact }">
49
+ <CmsEditorHistory
50
+ v-if="!readonly && !compact"
51
+ :can-undo="canUndo"
52
+ :can-redo="canRedo"
53
+ @undo="undo"
54
+ @redo="redo"
55
+ />
56
+ <slot name="actions" :compact="compact" />
57
+ </template>
58
+
59
+ <template #left-header>
60
+ <KPageHeader size="sm" :title="blocksField.label" :level="2" truncate>
61
+ <template #badge>
62
+ <KBadge size="sm" variant="outline" :label="blockCount" />
63
+ </template>
64
+ </KPageHeader>
65
+ </template>
66
+
67
+ <template #left>
68
+ <div class="cms-editor-tree">
69
+ <KFieldMessage v-if="blocksFieldError" :message="blocksFieldError" />
70
+ <KFieldMessage v-else tone="warning" :message="form.warnings[fieldKey]" />
71
+ <CmsBlockTree
72
+ :items="blocks"
73
+ :field="blocksField"
74
+ :field-key="fieldKey"
75
+ :locale="locale"
76
+ :selected="selected"
77
+ :highlighted="canvasHover"
78
+ :errors="blockErrors"
79
+ :can-add="canAddTo"
80
+ :label="blocksField.label"
81
+ :menu="blockMenu"
82
+ :readonly="readonly"
83
+ @select="selectFromTree"
84
+ @move="move"
85
+ @hover="highlight"
86
+ @insert="openPalette"
87
+ />
88
+ <template v-if="blocks.length && !readonly">
89
+ <KButton
90
+ v-if="canAdd"
91
+ :icon="cmsIcon('plus')"
92
+ label="Add a block"
93
+ size="sm"
94
+ variant="soft"
95
+ tone="neutral"
96
+ block
97
+ aria-haspopup="dialog"
98
+ @click="
99
+ (event) => openPalette([fieldKey], blocks.length, event.currentTarget)
100
+ "
101
+ />
102
+ <KFieldMessage v-else tone="neutral" :icon="false" :message="maxReached" />
103
+ </template>
10
104
  </div>
11
- <div class="cms-editor-bar-center">
12
- <CmsLocaleSwitch
13
- v-if="i18n.locales.length > 1"
14
- v-model="locale"
15
- :value="localeFilled"
105
+ </template>
106
+
107
+ <template #main-header="{ compact, collapsed, toggle }">
108
+ <div class="cms-editor-canvas-bar">
109
+ <KButton
110
+ v-if="!compact"
111
+ class="cms-editor-canvas-toggle"
112
+ :icon="cmsIcon('queue-list')"
113
+ size="sm"
114
+ :variant="collapsed.left ? 'ghost' : 'soft'"
115
+ tone="neutral"
116
+ aria-label="Blocks panel"
117
+ :tooltip="collapsed.left ? 'Show blocks' : 'Hide blocks'"
118
+ :aria-pressed="!collapsed.left"
119
+ @click="toggle('left')"
16
120
  />
17
- <KSegmentedControl
18
- v-model="device"
19
- :options="deviceOptions"
121
+ <div class="cms-editor-canvas-tools">
122
+ <span v-if="canvasWidth" class="cms-editor-width" aria-live="polite">
123
+ {{ canvasWidth }} px
124
+ </span>
125
+ <KButton
126
+ :icon="cmsIcon('arrow-path')"
127
+ size="sm"
128
+ variant="ghost"
129
+ tone="neutral"
130
+ aria-label="Reload preview"
131
+ @click="reloadPreview"
132
+ />
133
+ </div>
134
+ <KButton
135
+ v-if="!compact"
136
+ class="cms-editor-canvas-toggle is-end"
137
+ :icon="cmsIcon('adjustments-horizontal')"
20
138
  size="sm"
21
- icon-only
22
- aria-label="Preview width"
139
+ :variant="collapsed.right ? 'ghost' : 'soft'"
140
+ tone="neutral"
141
+ aria-label="Fields panel"
142
+ :tooltip="collapsed.right ? 'Show fields' : 'Hide fields'"
143
+ :aria-pressed="!collapsed.right"
144
+ @click="toggle('right')"
23
145
  />
24
146
  </div>
25
- <div class="cms-editor-bar-end">
26
- <slot name="actions" />
27
- </div>
28
- </header>
147
+ </template>
29
148
 
30
- <div class="cms-editor-body">
31
- <div class="cms-editor-canvas">
32
- <div class="cms-editor-frame" :class="`is-${device}`">
149
+ <template #main="{ compact }">
150
+ <div class="cms-editor-stage" :class="{ 'is-compact': compact }">
151
+ <div
152
+ ref="frameBox"
153
+ class="cms-editor-frame"
154
+ :class="{ 'is-fixed': deviceWidth !== null }"
155
+ :style="deviceWidth === null ? void 0 : { width: `${deviceWidth}px` }"
156
+ >
33
157
  <iframe
34
158
  ref="frame"
35
159
  :src="previewPath"
@@ -39,165 +163,124 @@
39
163
  />
40
164
  </div>
41
165
  </div>
166
+ </template>
167
+
168
+ <template v-if="selectedItem && selected" #right-header>
169
+ <KPageHeader
170
+ size="sm"
171
+ :title="selectedBlock?.label ?? String(selectedItem.type)"
172
+ :description="selectedSummary || selectedPosition"
173
+ :icon="cmsIcon(selectedBlock?.icon ?? 'rectangle-stack')"
174
+ :breadcrumbs="breadcrumbs"
175
+ breadcrumbs-ancestors
176
+ :level="2"
177
+ truncate
178
+ >
179
+ <template v-if="isHiddenBlock(selectedItem)" #badge>
180
+ <KBadge size="sm" label="Hidden" />
181
+ </template>
182
+ <template #actions>
183
+ <KDropdownMenu :items="blockMenu(selected)" placement="bottom-end">
184
+ <KButton
185
+ :icon="cmsIcon('ellipsis-horizontal')"
186
+ size="sm"
187
+ variant="ghost"
188
+ tone="neutral"
189
+ aria-label="Block actions"
190
+ />
191
+ </KDropdownMenu>
192
+ <KTooltip text="Close" shortcut="escape">
193
+ <KButton
194
+ :icon="cmsIcon('x-mark')"
195
+ size="sm"
196
+ variant="ghost"
197
+ tone="neutral"
198
+ aria-label="Close the block"
199
+ @click="select(null)"
200
+ />
201
+ </KTooltip>
202
+ </template>
203
+ </KPageHeader>
204
+ </template>
205
+
206
+ <template #right>
207
+ <KForm
208
+ as="div"
209
+ :form="form"
210
+ :alerts="false"
211
+ :disabled="readonly"
212
+ class="cms-editor-inspector"
213
+ >
214
+ <div v-if="selectedItem && selected" class="cms-editor-panel-body">
215
+ <KFormAlerts />
216
+ <KFieldMessage
217
+ v-if="form.errors[selectedName]"
218
+ :message="form.errors[selectedName]"
219
+ />
220
+ <KFieldMessage v-else tone="warning" :message="form.warnings[selectedName]" />
221
+ <CmsFormSections
222
+ v-if="selectedBlock"
223
+ ref="sectionsRef"
224
+ :key="selectedKey"
225
+ :layout="selectedLayout"
226
+ :shown="isBlockFieldShown"
227
+ >
228
+ <template #field="{ fieldKey: blockKey }">
229
+ <KFormField
230
+ :name="`${selectedName}.${blockKey}`"
231
+ :label="selectedBlock.fields[blockKey].label"
232
+ :help="selectedBlock.fields[blockKey].description"
233
+ :required="selectedBlock.fields[blockKey].required"
234
+ :highlight="highlighted === blockKey"
235
+ >
236
+ <CmsNestedBlockList
237
+ v-if="selectedBlock.fields[blockKey].type === 'blocks'"
238
+ :field="selectedBlock.fields[blockKey]"
239
+ :items="selectedItem[blockKey]"
240
+ :locale="locale"
241
+ :invalid="(index) => childInvalid(blockKey, index)"
242
+ :readonly="readonly"
243
+ @select="(index) => selectChild(blockKey, index)"
244
+ @insert="(anchor) => insertChild(blockKey, anchor)"
245
+ />
246
+ <CmsFieldInput
247
+ v-else
248
+ :model-value="selectedItem[blockKey]"
249
+ :field="selectedBlock.fields[blockKey]"
250
+ :locale="locale"
251
+ @update:model-value="
252
+ (value) => updateBlockField(blockKey, value)
253
+ "
254
+ />
255
+ </KFormField>
256
+ </template>
257
+ </CmsFormSections>
258
+ </div>
42
259
 
43
- <aside class="cms-editor-panel" aria-label="Inspector">
44
260
  <component
45
261
  :is="tabbed ? KTabs : 'div'"
262
+ v-else-if="content"
46
263
  v-bind="tabsBinding"
47
- class="cms-editor-inspector"
264
+ class="cms-editor-tabs"
48
265
  >
49
266
  <template #[panelSlot]>
50
- <KForm ref="panel" as="div" :form="form" class="cms-editor-panel-body">
51
- <template v-if="activeTab === 'block'">
52
- <template v-if="selectedItem && selected !== null">
53
- <div class="cms-editor-block-head">
54
- <KButton
55
- icon="arrow-left"
56
- size="sm"
57
- variant="ghost"
58
- tone="neutral"
59
- label="All blocks"
60
- @click="select(null)"
61
- />
62
- <span class="cms-editor-block-position k-mono">
63
- {{ selected + 1 }} / {{ blocks.length }}
64
- </span>
65
- </div>
66
- <div class="cms-editor-block-title">
67
- <KIcon :name="cmsIcon(selectedBlock?.icon ?? 'rectangle-stack')" />
68
- <span>{{ selectedBlock?.label ?? selectedItem.type }}</span>
69
- <KBadge v-if="isHiddenBlock(selectedItem)" size="sm" label="Hidden" />
70
- </div>
71
- <KStack direction="row" gap="sm" wrap class="cms-editor-block-actions">
72
- <KButton
73
- icon="arrow-up"
74
- size="sm"
75
- variant="soft"
76
- tone="neutral"
77
- aria-label="Move up"
78
- title="Move up"
79
- :disabled="selected === 0"
80
- @click="moveSelected(-1)"
81
- />
82
- <KButton
83
- icon="arrow-down"
84
- size="sm"
85
- variant="soft"
86
- tone="neutral"
87
- aria-label="Move down"
88
- title="Move down"
89
- :disabled="selected === blocks.length - 1"
90
- @click="moveSelected(1)"
91
- />
92
- <KButton
93
- :icon="cmsIcon('document-duplicate')"
94
- size="sm"
95
- variant="soft"
96
- tone="neutral"
97
- label="Duplicate"
98
- :disabled="!canAdd"
99
- @click="duplicate(selected)"
100
- />
101
- <KButton
102
- v-if="canHideBlock(selectedBlock)"
103
- :icon="cmsIcon(isHiddenBlock(selectedItem) ? 'eye' : 'eye-slash')"
104
- size="sm"
105
- variant="soft"
106
- tone="neutral"
107
- :label="isHiddenBlock(selectedItem) ? 'Show' : 'Hide'"
108
- @click="toggleHidden(selected)"
109
- />
110
- <KButton
111
- icon="trash"
112
- size="sm"
113
- variant="soft"
114
- tone="danger"
115
- aria-label="Delete block"
116
- title="Delete"
117
- @click="remove(selected)"
118
- />
119
- </KStack>
120
- <KFieldMessage
121
- v-if="form.errors[`${fieldKey}.${selected}`]"
122
- :message="form.errors[`${fieldKey}.${selected}`]"
123
- />
124
- <KFieldMessage
125
- v-else
126
- tone="warning"
127
- :message="form.warnings[`${fieldKey}.${selected}`]"
128
- />
129
- <KStack gap="md" class="cms-editor-fields">
130
- <KFormField
131
- v-for="(blockField, blockKey) in selectedBlock?.fields ?? {}"
132
- :key="`${selected}-${blockKey}`"
133
- :name="`${fieldKey}.${selected}.${blockKey}`"
134
- :label="blockField.label"
135
- :description="blockField.description"
136
- :icon="cmsIcon(fieldIcon(blockField))"
137
- :required="blockField.required"
138
- >
139
- <CmsFieldInput
140
- :model-value="selectedItem[blockKey]"
141
- :field="blockField"
142
- :locale="locale"
143
- @update:model-value="
144
- (value) => updateBlockField(String(blockKey), value)
145
- "
146
- />
147
- </KFormField>
148
- </KStack>
149
- </template>
150
-
151
- <template v-else>
152
- <KFieldMessage
153
- v-if="form.errors[fieldKey]"
154
- :message="form.errors[fieldKey]"
155
- />
156
- <KFieldMessage
157
- v-else
158
- tone="warning"
159
- :message="form.warnings[fieldKey]"
160
- />
161
- <KStack v-if="blocks.length" gap="sm">
162
- <span class="k-label">{{ blocksField.label }}</span>
163
- <CmsBlockOutline
164
- :items="blocks"
165
- :field="blocksField"
166
- :locale="locale"
167
- :selected="selected"
168
- @select="select"
169
- @move="move"
170
- />
171
- </KStack>
172
- <KStack v-if="canAdd" gap="sm">
173
- <span class="k-label">Add a block</span>
174
- <CmsBlockPalette
175
- :blocks="blocksField.blocks ?? {}"
176
- @pick="(type) => insert(blocks.length, type)"
177
- />
178
- </KStack>
179
- <p v-else class="cms-field-hint">{{ maxReached }}</p>
180
- </template>
181
- </template>
182
-
183
- <KStack v-else gap="md" class="cms-editor-fields">
184
- <template v-for="section in sections" :key="section.id">
185
- <div v-if="section.title" class="cms-editor-section-title">
186
- <span class="k-label">{{ section.title }}</span>
187
- <span v-if="section.description" class="cms-field-hint">
188
- {{ section.description }}
189
- </span>
190
- </div>
267
+ <div class="cms-editor-panel-body">
268
+ <KFormAlerts />
269
+ <slot v-if="activeTab === 'content'" name="content-start" :locale="locale" />
270
+ <CmsFormSections
271
+ ref="sectionsRef"
272
+ :key="activeTab"
273
+ :layout="tabLayout"
274
+ :shown="isShown"
275
+ >
276
+ <template #field="{ fieldKey: key }">
191
277
  <KFormField
192
- v-for="key in section.keys"
193
- :key="key"
194
278
  :name="key"
195
279
  :label="fields[key].label"
196
- :description="fields[key].description"
197
- :icon="cmsIcon(fieldIcon(fields[key]))"
280
+ :help="fields[key].description"
198
281
  :required="fields[key].required"
199
- :data-cms-field="key"
200
- :class="{ 'cms-search-focus': highlighted === key }"
282
+ :disabled="locked?.includes(key) || void 0"
283
+ :highlight="highlighted === key"
201
284
  >
202
285
  <CmsFieldInput
203
286
  :model-value="state[key]"
@@ -206,73 +289,163 @@
206
289
  :slug-source="slugSource(fields[key])"
207
290
  :entry-name="entryName"
208
291
  :field-key="key"
209
- @update:model-value="(value) => setValue(key, value)"
292
+ @update:model-value="(value) => setValue(key, value, key)"
210
293
  />
211
294
  </KFormField>
212
295
  </template>
213
- </KStack>
214
- </KForm>
296
+ </CmsFormSections>
297
+ </div>
215
298
  </template>
216
299
  </component>
217
- </aside>
218
- </div>
219
300
 
220
- <KModal v-model:open="paletteOpen" title="Insert a block">
221
- <CmsBlockPalette :blocks="blocksField.blocks ?? {}" @pick="insertFromPalette" />
222
- </KModal>
223
- </div>
301
+ <div v-else class="cms-editor-panel-body is-empty">
302
+ <KFormAlerts />
303
+ <KEmptyState
304
+ :icon="cmsIcon('cursor-arrow-rays')"
305
+ title="No block selected"
306
+ description="Pick a block in the list or in the preview to edit its fields."
307
+ variant="plain"
308
+ size="sm"
309
+ />
310
+ </div>
311
+ </KForm>
312
+ </template>
313
+ </KWorkspaceLayout>
314
+
315
+ <CmsBlockPickerPopover
316
+ v-model:open="paletteOpen"
317
+ :anchor="paletteAnchor"
318
+ :blocks="pickerField?.blocks ?? {}"
319
+ :disabled-reason="canAddTo(picker.list) ? void 0 : maxReachedIn(picker.list)"
320
+ :paste-count="pasteCount"
321
+ :presets="presets"
322
+ @pick="insertFromPalette"
323
+ @paste="pasteFromPalette"
324
+ @preset="insertPresetFromPalette"
325
+ />
326
+
327
+ <CmsPresetModal
328
+ v-model:open="presetOpen"
329
+ title="Save as preset"
330
+ submit-label="Save preset"
331
+ :hint="presetHint"
332
+ :save="savePreset"
333
+ />
224
334
  </template>
225
335
 
226
336
  <script setup>
227
337
  import {
338
+ blockAt,
339
+ blockFormLayout,
340
+ blockListAt,
341
+ blockListPath,
342
+ blockPathIndex,
343
+ blockSchemaPath,
344
+ blocksFieldAt,
228
345
  canAddBlock,
346
+ formatBlockPath,
347
+ insertBlocks,
348
+ isBlockListPath,
229
349
  isFieldVisible,
230
350
  isHiddenBlock,
231
351
  isTranslatableField,
232
- slugSourceValue
352
+ moveBlock,
353
+ movedBlockPath,
354
+ parentBlockPath,
355
+ pathAfterInsert,
356
+ pathAfterMove,
357
+ pathAfterRemove,
358
+ remapInsertionPoint,
359
+ removeBlock,
360
+ resolveBlockPath,
361
+ slugSourceValue,
362
+ updateBlockAt,
363
+ updateBlockList
233
364
  } from "#nuxt-cms";
234
365
  import {
235
366
  Badge as KBadge,
367
+ Breadcrumbs as KBreadcrumbs,
236
368
  Button as KButton,
369
+ DropdownMenu as KDropdownMenu,
370
+ EmptyState as KEmptyState,
237
371
  FieldMessage as KFieldMessage,
238
372
  Form as KForm,
373
+ FormAlerts as KFormAlerts,
239
374
  FormField as KFormField,
240
- Icon as KIcon,
241
- Modal as KModal,
375
+ PageHeader as KPageHeader,
242
376
  SegmentedControl as KSegmentedControl,
243
- Stack as KStack,
244
377
  Tabs as KTabs,
245
- useForm
378
+ Tooltip as KTooltip,
379
+ WorkspaceLayout as KWorkspaceLayout,
380
+ useFieldHighlight,
381
+ useForm,
382
+ useTheme
246
383
  } from "@xleddyl/443-components-vue";
247
- import { computed, onBeforeUnmount, onMounted, ref, shallowRef, watch } from "#imports";
384
+ import {
385
+ computed,
386
+ nextTick,
387
+ onBeforeUnmount,
388
+ onMounted,
389
+ ref,
390
+ shallowRef,
391
+ useTemplateRef,
392
+ watch
393
+ } from "#imports";
248
394
  import cmsConfig from "#cms-config";
395
+ import { CMS_EDITOR_PANES, CMS_PREVIEW_DEVICE_WIDTHS } from "../../../shared/editor-layout";
249
396
  import { buildEntrySchema, entryValidationWarnings } from "../../../shared/validation";
397
+ import {
398
+ clampInlineText,
399
+ inlineFieldPath,
400
+ inlineFieldValue,
401
+ inlineTextLimit,
402
+ isInlineTextField,
403
+ lockedInlineFields,
404
+ normalizeInlineText
405
+ } from "../../../shared/inline-text";
406
+ import { copyNotice, pasteBlocks, pasteNotice, pasteableCount } from "../../../shared/clipboard";
407
+ import { CMS_PRESET_ICON, presetInsertNotice } from "../../../shared/presets";
250
408
  import {
251
409
  DEFAULT_CMS_PREVIEW_PATH,
410
+ blockErrorPaths,
411
+ blockSummary,
252
412
  canHideBlock,
413
+ canMoveBlock,
414
+ clipboardActionApplies,
253
415
  collectMediaKeys,
254
- duplicateBlock,
416
+ duplicateBlockAt,
417
+ editorIssueKeys,
418
+ editorShortcut,
255
419
  emptyBlock,
256
- insertBlock,
420
+ firstBlockError,
257
421
  isTrustedPreviewEvent,
258
- moveBlock,
259
422
  parseFrameMessage,
260
423
  previewEnvelope,
424
+ previewFieldConfig,
261
425
  previewMedia,
262
- removeBlock,
263
- selectionAfterMove,
264
- selectionAfterRemove,
265
- toggleBlockHidden
426
+ shortcutApplies,
427
+ toggleBlockHiddenAt
266
428
  } from "../../../shared/preview";
267
- import { useCmsConfirm } from "../../composables/cms-confirm";
268
- import { useCmsFieldFocus } from "../../composables/cms-field-focus";
429
+ import { translationProgress } from "../../../shared/translations";
430
+ import { useCmsClipboard } from "../../composables/cms-clipboard";
431
+ import { useCmsHistory } from "../../composables/cms-history";
432
+ import { useCmsPermissions } from "../../composables/cms-permissions";
433
+ import { useCmsPresets } from "../../composables/cms-presets";
434
+ import { useCmsPageQuery } from "../../composables/cms-route";
269
435
  import { useCmsRuntime } from "../../composables/cms-runtime";
270
436
  import { entryFormLayout, useCmsSettingsState } from "../../composables/cms-settings";
271
- import { cmsIcon, fieldIcon } from "../../utils/ui";
272
- const SEO_KEYS = ["seoTitle", "seoDescription", "seoImage"];
437
+ import {
438
+ clipboardTarget,
439
+ isInForeignOverlay,
440
+ onApplePlatform,
441
+ shortcutTarget
442
+ } from "../../utils/keyboard";
443
+ import { cmsIcon } from "../../utils/ui";
444
+ const SEO_KEYS = ["seoTitle", "seoDescription", "seoImage", "seoNoindex"];
273
445
  const deviceOptions = [
274
446
  { value: "desktop", label: "Desktop width", icon: cmsIcon("computer-desktop") },
275
- { value: "mobile", label: "Mobile width", icon: cmsIcon("device-phone-mobile") }
447
+ { value: "tablet", label: "Tablet width, 768 px", icon: cmsIcon("device-tablet") },
448
+ { value: "mobile", label: "Mobile width, 390 px", icon: cmsIcon("device-phone-mobile") }
276
449
  ];
277
450
  const props = defineProps({
278
451
  entryName: { type: String, required: true },
@@ -280,71 +453,148 @@ const props = defineProps({
280
453
  fields: { type: Object, required: true },
281
454
  title: { type: String, required: true },
282
455
  content: { type: Boolean, required: false },
283
- focus: { type: [Object, null], required: false }
456
+ focus: { type: [Object, null], required: false },
457
+ locked: { type: Array, required: false },
458
+ readonly: { type: Boolean, required: false },
459
+ tabQuery: { type: String, required: false },
460
+ trail: { type: Array, required: false }
284
461
  });
285
462
  const state = defineModel("state", { type: Object, ...{ required: true } });
463
+ const emit = defineEmits(["save"]);
286
464
  const runtime = useCmsRuntime();
287
465
  const { i18n } = runtime;
288
466
  const previewPath = runtime.previewPath ?? DEFAULT_CMS_PREVIEW_PATH;
289
- const confirmAction = useCmsConfirm();
290
467
  const settings = useCmsSettingsState();
468
+ const workspace = useTemplateRef("workspace");
291
469
  const locale = ref(i18n.defaultLocale);
292
470
  const device = ref("desktop");
293
- const selected = ref(null);
471
+ const pane = ref("main");
472
+ const inspectorCollapsed = ref(false);
473
+ const selected = shallowRef(null);
474
+ const canvasHover = shallowRef(null);
475
+ const canvasWidth = ref(null);
294
476
  const paletteOpen = ref(false);
295
- const paletteIndex = ref(0);
477
+ const picker = shallowRef({
478
+ list: [props.fieldKey],
479
+ index: 0
480
+ });
481
+ const paletteAnchor = shallowRef(null);
296
482
  const frame = ref(null);
483
+ const frameBox = ref(null);
297
484
  const library = shallowRef(/* @__PURE__ */ new Map());
485
+ const deviceWidth = computed(() => CMS_PREVIEW_DEVICE_WIDTHS[device.value]);
298
486
  const blocksField = computed(() => props.fields[props.fieldKey]);
299
487
  const blocks = computed(() => {
300
488
  const value = state.value[props.fieldKey];
301
489
  return Array.isArray(value) ? value : [];
302
490
  });
303
- const canAdd = computed(() => canAddBlock(blocksField.value, blocks.value.length));
304
- const maxReached = computed(() => {
305
- const max = blocksField.value.max ?? 0;
491
+ function inField(path) {
492
+ return !!path && path[0] === props.fieldKey;
493
+ }
494
+ function listField(list) {
495
+ return inField(list) ? blocksFieldAt(props.fields, state.value, list) : null;
496
+ }
497
+ function listLength(list) {
498
+ return blockListAt(state.value, list)?.length ?? 0;
499
+ }
500
+ function canAddTo(list) {
501
+ const field = listField(list);
502
+ return !!field && !props.readonly && canAddBlock(field, listLength(list));
503
+ }
504
+ function maxReachedIn(list) {
505
+ const max = listField(list)?.max ?? 0;
306
506
  return `This field holds at most ${max} ${max === 1 ? "block" : "blocks"}.`;
507
+ }
508
+ const canAdd = computed(() => canAddBlock(blocksField.value, blocks.value.length));
509
+ const blockCount = computed(() => {
510
+ const max = blocksField.value.max;
511
+ return max === void 0 ? String(blocks.value.length) : `${blocks.value.length} / ${max}`;
307
512
  });
308
- const selectedItem = computed(
309
- () => selected.value === null ? void 0 : blocks.value[selected.value]
310
- );
311
- const selectedBlock = computed(
312
- () => selectedItem.value ? blocksField.value.blocks?.[String(selectedItem.value.type)] : void 0
513
+ const maxReached = computed(() => maxReachedIn([props.fieldKey]));
514
+ function locate(path) {
515
+ return inField(path) ? resolveBlockPath(props.fields, state.value, path) : null;
516
+ }
517
+ const selectedLocation = computed(() => locate(selected.value));
518
+ const selectedItem = computed(() => selectedLocation.value?.item);
519
+ const selectedBlock = computed(() => selectedLocation.value?.block);
520
+ const selectedName = computed(() => selected.value ? formatBlockPath(selected.value) : "");
521
+ const selectedSummary = computed(
522
+ () => selectedItem.value ? blockSummary(selectedBlock.value, selectedItem.value, locale.value, i18n.defaultLocale) : ""
313
523
  );
524
+ const selectedPosition = computed(() => {
525
+ const path = selected.value;
526
+ if (!path) return "";
527
+ return `Block ${blockPathIndex(path) + 1} of ${listLength(blockListPath(path))}`;
528
+ });
529
+ function blockLabelAt(path) {
530
+ const location = locate(path);
531
+ return location?.block?.label ?? String(location?.item.type ?? "");
532
+ }
533
+ const breadcrumbs = computed(() => {
534
+ const path = selected.value;
535
+ if (!path) return [];
536
+ const items = [];
537
+ for (let end = 2; end < path.length; end += 2) {
538
+ const ancestor = path.slice(0, end);
539
+ items.push({ label: blockLabelAt(ancestor), onClick: () => selectFromTree(ancestor) });
540
+ }
541
+ return items;
542
+ });
543
+ const seoKeys = computed(() => SEO_KEYS.filter((key) => Object.hasOwn(props.fields, key)));
314
544
  const tabList = computed(() => {
315
- if (!props.content) return [{ id: "block", label: "Block" }];
316
- const tabs = [
317
- { id: "content", label: "Content" },
318
- { id: "block", label: "Block" }
319
- ];
545
+ const tabs = [{ id: "content", label: "Content" }];
320
546
  if (seoKeys.value.length) tabs.push({ id: "seo", label: "SEO" });
321
547
  return tabs;
322
548
  });
323
- const activeTab = ref(props.content ? "content" : "block");
549
+ const localTab = ref("content");
550
+ const routeTab = props.tabQuery ? useCmsPageQuery(props.tabQuery) : null;
551
+ const activeTab = computed({
552
+ get: () => {
553
+ const wanted = routeTab ? routeTab.value : localTab.value;
554
+ return tabList.value.some((tab) => tab.id === wanted) ? wanted : "content";
555
+ },
556
+ set: (value) => {
557
+ if (!routeTab) localTab.value = value;
558
+ else routeTab.value = value === "content" ? "" : value;
559
+ }
560
+ });
324
561
  const tabbed = computed(() => tabList.value.length > 1);
325
562
  const panelSlot = computed(() => tabbed.value ? "panel" : "default");
563
+ function errorBadge(count) {
564
+ return {
565
+ badge: count || void 0,
566
+ badgeTone: "danger",
567
+ badgeLabel: count ? `${count} ${count === 1 ? "error" : "errors"}` : void 0
568
+ };
569
+ }
326
570
  const tabsBinding = computed(
327
571
  () => tabbed.value ? {
328
- items: tabList.value.map((tab) => {
329
- const errorCount = tabErrorCount(tab.id);
330
- return {
331
- value: tab.id,
332
- label: tab.label,
333
- badge: errorCount || void 0,
334
- badgeTone: "danger",
335
- badgeLabel: errorCount ? `${errorCount} ${errorCount === 1 ? "error" : "errors"}` : void 0
336
- };
337
- }),
572
+ items: tabList.value.map((tab) => ({
573
+ value: tab.id,
574
+ label: tab.label,
575
+ ...errorBadge(tabErrorCount(tab.id))
576
+ })),
577
+ label: "Entry fields",
338
578
  modelValue: activeTab.value,
339
579
  panelFocusable: false,
340
580
  stretch: true,
341
581
  inset: true,
582
+ fill: true,
342
583
  "onUpdate:modelValue": (value) => {
343
- activeTab.value = String(value ?? "");
584
+ activeTab.value = value === "seo" ? "seo" : "content";
344
585
  }
345
586
  } : {}
346
587
  );
347
- const seoKeys = computed(() => SEO_KEYS.filter((key) => Object.hasOwn(props.fields, key)));
588
+ const leftPane = computed(() => ({
589
+ label: "Blocks",
590
+ ...CMS_EDITOR_PANES.left,
591
+ ...errorBadge(topLevelErrorCount.value + (blocksFieldError.value ? 1 : 0))
592
+ }));
593
+ const rightPane = computed(() => ({
594
+ label: "Fields",
595
+ ...CMS_EDITOR_PANES.right,
596
+ ...errorBadge(tabErrorCount("content") + tabErrorCount("seo"))
597
+ }));
348
598
  const contentFields = computed(
349
599
  () => Object.fromEntries(
350
600
  Object.entries(props.fields).filter(
@@ -352,85 +602,481 @@ const contentFields = computed(
352
602
  )
353
603
  )
354
604
  );
355
- const sections = computed(() => {
356
- if (activeTab.value === "seo")
357
- return [{ id: "seo", title: void 0, description: void 0, keys: seoKeys.value }];
358
- const layout = entryFormLayout(props.entryName, settings.value, contentFields.value);
359
- return layout.main.map((section, index) => ({
360
- id: `${index}-${section.title ?? ""}`,
361
- title: section.title,
362
- description: section.description,
363
- keys: section.rows.flat().filter((key) => {
364
- const field = props.fields[key];
365
- return !!field && isFieldVisible(field, state.value);
366
- })
367
- })).filter((section) => section.keys.length);
368
- });
369
- const localeFilled = computed(() => {
370
- const title = props.content ? state.value.title : null;
371
- return title && typeof title === "object" ? title : null;
372
- });
373
- function setValue(key, value) {
374
- state.value = { ...state.value, [key]: value };
605
+ const tabLayout = computed(
606
+ () => activeTab.value === "seo" ? { main: [{ rows: seoKeys.value.map((key) => [key]) }] } : entryFormLayout(props.entryName, settings.value, contentFields.value)
607
+ );
608
+ function isShown(key) {
609
+ const field = props.fields[key];
610
+ return !!field && isFieldVisible(field, state.value, props.fields);
611
+ }
612
+ function isBlockFieldShown(key) {
613
+ const fields = selectedBlock.value?.fields ?? {};
614
+ const field = fields[key];
615
+ return !!field && isFieldVisible(field, selectedItem.value, fields);
616
+ }
617
+ const selectedKey = computed(
618
+ () => selected.value ? `${selectedName.value}-${String(selectedItem.value?.type)}` : ""
619
+ );
620
+ const selectedLayout = computed(
621
+ () => selectedBlock.value ? blockFormLayout(selectedBlock.value) : { main: [] }
622
+ );
623
+ const sectionsRef = ref(null);
624
+ function expandSection(key) {
625
+ void nextTick(() => sectionsRef.value?.reveal(key));
626
+ }
627
+ function revealInPanel(key) {
628
+ void nextTick(() => {
629
+ sectionsRef.value?.reveal(key);
630
+ highlightField(key);
631
+ });
632
+ }
633
+ const translations = computed(() => translationProgress(props.fields, state.value, i18n));
634
+ let ownBlocks = state.value[props.fieldKey];
635
+ const pendingPastes = /* @__PURE__ */ new Set();
636
+ function setValue(key, value, coalesce, hold = false) {
637
+ if (props.readonly) return;
638
+ change(coalesce, hold);
639
+ const next = { ...state.value, [key]: value };
640
+ ownBlocks = next[props.fieldKey];
641
+ state.value = next;
642
+ }
643
+ function commitValues(next, coalesce, hold = false) {
644
+ if (props.readonly || next === state.value) return false;
645
+ change(coalesce, hold);
646
+ ownBlocks = next[props.fieldKey];
647
+ state.value = next;
648
+ return true;
649
+ }
650
+ function dropInsertionPoints() {
651
+ paletteOpen.value = false;
652
+ picker.value = { list: [props.fieldKey], index: 0 };
653
+ pendingPastes.clear();
375
654
  }
376
- function setBlocks(list) {
377
- setValue(props.fieldKey, list.length ? list : null);
655
+ function remapPaths(remap) {
656
+ if (selected.value) selected.value = remap(selected.value);
657
+ canvasHover.value = null;
658
+ const target = remapInsertionPoint(picker.value, remap);
659
+ if (target) picker.value = target;
660
+ else {
661
+ paletteOpen.value = false;
662
+ picker.value = { list: [props.fieldKey], index: 0 };
663
+ }
664
+ for (const point of [...pendingPastes]) {
665
+ pendingPastes.delete(point);
666
+ const next = remapInsertionPoint(point, remap);
667
+ if (next) pendingPastes.add(Object.assign(point, next));
668
+ }
378
669
  }
379
670
  function slugSource(field) {
380
671
  if (!field.from) return void 0;
381
672
  return slugSourceValue(props.fields[field.from], state.value[field.from], i18n.defaultLocale);
382
673
  }
383
- function select(index) {
384
- selected.value = index;
385
- if (selected.value !== null) activeTab.value = "block";
674
+ function select(path) {
675
+ selected.value = inField(path) ? path : null;
676
+ }
677
+ function isCompact() {
678
+ return !!workspace.value?.compact;
679
+ }
680
+ function showPanel(name) {
681
+ workspace.value?.reveal(name);
682
+ }
683
+ function selectFromTree(path) {
684
+ select(locate(path) ? path : null);
685
+ if (selected.value) post({ type: "reveal", path: selected.value });
686
+ if (isCompact()) pane.value = "right";
386
687
  }
387
- function blockConfigAt(index) {
388
- return blocksField.value.blocks?.[String(blocks.value[index]?.type)];
688
+ function highlight(path) {
689
+ post({ type: "highlight", path });
389
690
  }
390
- function insert(index, type) {
391
- const block = blocksField.value.blocks?.[type];
392
- if (!block || !canAdd.value) return;
393
- const at = Math.min(Math.max(index, 0), blocks.value.length);
394
- setBlocks(insertBlock(blocks.value, at, emptyBlock(type, block)));
395
- select(at);
691
+ function insert(list, index, type) {
692
+ const field = listField(list);
693
+ const block = field?.blocks?.[type];
694
+ if (!block || !canAddTo(list)) return;
695
+ const at = Math.min(Math.max(index, 0), listLength(list));
696
+ if (!commitValues(insertBlocks(state.value, list, at, [emptyBlock(type, block)]))) return;
697
+ remapPaths((path) => pathAfterInsert(path, list, at));
698
+ select([...list, at]);
396
699
  }
397
700
  function insertFromPalette(type) {
398
701
  paletteOpen.value = false;
399
- insert(paletteIndex.value, type);
702
+ insert(picker.value.list, picker.value.index, type);
400
703
  }
401
- function openPalette(index) {
402
- if (!canAdd.value) return;
403
- paletteIndex.value = index;
704
+ function pasteFromPalette() {
705
+ paletteOpen.value = false;
706
+ void paste(picker.value.list, picker.value.index, null);
707
+ }
708
+ function openPalette(list, index, anchor = null) {
709
+ if (props.readonly || !canAddTo(list)) return;
710
+ picker.value = { list, index };
711
+ presetList.value = list;
712
+ paletteAnchor.value = anchor ?? canvasAnchor([...list, index]) ?? focusedElement() ?? frame.value;
404
713
  paletteOpen.value = true;
405
714
  }
406
- function move(from, to) {
407
- if (to < 0 || to >= blocks.value.length || from === to) return;
408
- setBlocks(moveBlock(blocks.value, from, to));
409
- selected.value = selectionAfterMove(selected.value, from, to);
715
+ const pickerField = computed(() => listField(picker.value.list));
716
+ function insertButton(path) {
717
+ let element = null;
718
+ try {
719
+ element = frame.value?.contentDocument?.querySelector(
720
+ `[data-cms-insert="${formatBlockPath(path)}"]`
721
+ ) ?? null;
722
+ } catch {
723
+ return null;
724
+ }
725
+ return element?.getClientRects().length ? element : null;
726
+ }
727
+ function canvasAnchor(path) {
728
+ const element = insertButton(path);
729
+ const host = frame.value;
730
+ if (!element || !host) return null;
731
+ let last = new DOMRect();
732
+ return {
733
+ getBoundingClientRect() {
734
+ if (!element.isConnected || !host.isConnected) return last;
735
+ const box = host.getBoundingClientRect();
736
+ const rect = element.getBoundingClientRect();
737
+ last = new DOMRect(
738
+ box.left + host.clientLeft + rect.left,
739
+ box.top + host.clientTop + rect.top,
740
+ rect.width,
741
+ rect.height
742
+ );
743
+ return last;
744
+ }
745
+ };
746
+ }
747
+ function focusedElement() {
748
+ const element = document.activeElement;
749
+ return element instanceof HTMLElement && element !== document.body ? element : null;
750
+ }
751
+ function move(from, toList, toIndex) {
752
+ if (!inField(from) || !inField(toList) || !isBlockListPath(toList)) return false;
753
+ if (!canMoveBlock(props.fields, state.value, from, toList)) return false;
754
+ if (!commitValues(moveBlock(state.value, from, toList, toIndex))) return false;
755
+ remapPaths((path) => pathAfterMove(path, from, toList, toIndex));
756
+ return true;
757
+ }
758
+ function moveBy(path, delta) {
759
+ const index = blockPathIndex(path);
760
+ const target = index + delta;
761
+ if (target < 0 || target >= listLength(blockListPath(path))) return;
762
+ move(path, blockListPath(path), delta > 0 ? target + 1 : target);
763
+ }
764
+ function duplicate(path) {
765
+ const list = blockListPath(path);
766
+ if (!canAddTo(list) || !commitValues(duplicateBlockAt(state.value, path))) return;
767
+ const at = blockPathIndex(path) + 1;
768
+ remapPaths((current) => pathAfterInsert(current, list, at));
769
+ select([...list, at]);
770
+ }
771
+ function toggleHidden(path) {
772
+ commitValues(toggleBlockHiddenAt(state.value, path, locate(path)?.block));
773
+ }
774
+ function remove(path) {
775
+ if (!locate(path) || !commitValues(removeBlock(state.value, path))) return;
776
+ remapPaths((current) => pathAfterRemove(current, path));
777
+ }
778
+ function selectChild(key, index) {
779
+ if (selected.value) selectFromTree([...selected.value, key, index]);
780
+ }
781
+ function insertChild(key, anchor) {
782
+ if (!selected.value) return;
783
+ const list = [...selected.value, key];
784
+ openPalette(list, listLength(list), anchor);
785
+ }
786
+ function childInvalid(key, index) {
787
+ return blockErrors.value.has(`${selectedName.value}.${key}.${index}`);
788
+ }
789
+ const clipboard = useCmsClipboard();
790
+ const PASTE_CANCELLED = {
791
+ title: "Nothing pasted",
792
+ description: "The blocks changed while the clipboard was read. Paste again.",
793
+ color: "error"
794
+ };
795
+ const pasteCount = computed(
796
+ () => pickerField.value ? pasteableCount(clipboard.items.value, pickerField.value) : 0
797
+ );
798
+ function copy(path, event) {
799
+ const item = locate(path)?.item;
800
+ if (!item) return;
801
+ clipboard.copy([item], event);
802
+ clipboard.notify(copyNotice(1));
803
+ }
804
+ function cut(path, event) {
805
+ const item = locate(path)?.item;
806
+ if (!item || props.readonly) return;
807
+ clipboard.copy([item], event);
808
+ remove(path);
809
+ clipboard.notify(copyNotice(1, true));
810
+ }
811
+ function insertItems(list, index, items) {
812
+ const field = listField(list);
813
+ if (!field) return null;
814
+ const current = blockListAt(state.value, list) ?? [];
815
+ const result = pasteBlocks(current, index, items, field, i18n);
816
+ if (result.pasted && commitValues(updateBlockList(state.value, list, () => result.list))) {
817
+ remapPaths((path) => pathAfterInsert(path, list, result.at, result.pasted));
818
+ select([...list, result.at]);
819
+ if (selected.value) post({ type: "reveal", path: selected.value });
820
+ }
821
+ return result;
822
+ }
823
+ async function paste(list, index, text) {
824
+ if (props.readonly || !listField(list)) return;
825
+ const point = { list, index };
826
+ pendingPastes.add(point);
827
+ let cancelled = false;
828
+ let items;
829
+ try {
830
+ items = await clipboard.read(text);
831
+ } finally {
832
+ cancelled = !pendingPastes.delete(point);
833
+ }
834
+ const field = listField(point.list);
835
+ if (cancelled || !field) {
836
+ clipboard.notify(PASTE_CANCELLED);
837
+ return;
838
+ }
839
+ const result = items ? insertItems(point.list, point.index, items) : null;
840
+ clipboard.notify(pasteNotice(result, field));
841
+ }
842
+ const { can } = useCmsPermissions();
843
+ const presetsAllowed = computed(() => !props.readonly && can(`${props.entryName}.update`));
844
+ const presetList = shallowRef([props.fieldKey]);
845
+ const presetFieldKey = computed(() => {
846
+ const list = presetList.value;
847
+ return inField(list) ? blockSchemaPath(props.fields, state.value, list)?.join(".") : void 0;
848
+ });
849
+ const {
850
+ presets,
851
+ load: loadPresets,
852
+ create: createPreset
853
+ } = useCmsPresets(
854
+ () => presetsAllowed.value ? props.entryName : void 0,
855
+ () => presetFieldKey.value
856
+ );
857
+ const presetOpen = ref(false);
858
+ const presetItems = ref([]);
859
+ const presetField = shallowRef(null);
860
+ const presetHint = computed(() => {
861
+ const types = presetItems.value.map(
862
+ (item) => presetField.value?.blocks?.[String(item.type)]?.label ?? String(item.type)
863
+ );
864
+ return `A copy of ${types.join(
865
+ ", "
866
+ )}. Insert it from the block picker of any field with the same blocks.`;
867
+ });
868
+ function openPresetSave(path) {
869
+ const location = locate(path);
870
+ if (!location) return;
871
+ presetList.value = blockListPath(path);
872
+ presetField.value = location.field;
873
+ presetItems.value = [JSON.parse(JSON.stringify(location.item))];
874
+ presetOpen.value = true;
875
+ }
876
+ async function savePreset(details) {
877
+ const preset = await createPreset({ ...details, items: presetItems.value });
878
+ clipboard.notify({ title: `Preset "${preset.name}" saved`, color: "success" });
410
879
  }
411
- function moveSelected(delta) {
412
- if (selected.value === null) return;
413
- move(selected.value, selected.value + delta);
880
+ function insertPresetFromPalette(id) {
881
+ paletteOpen.value = false;
882
+ const preset = presets.value.find((candidate) => candidate.id === id);
883
+ const { list, index } = picker.value;
884
+ const field = listField(list);
885
+ if (!preset || !field || !canAddTo(list)) return;
886
+ const result = insertItems(list, index, preset.items);
887
+ if (result) clipboard.notify(presetInsertNotice(result, field, preset));
888
+ }
889
+ watch(paletteOpen, (open) => {
890
+ if (open) void loadPresets();
891
+ });
892
+ function selectedPath() {
893
+ return selected.value && locate(selected.value) ? selected.value : null;
894
+ }
895
+ function after(path) {
896
+ return path ? [blockListPath(path), blockPathIndex(path) + 1] : [[props.fieldKey], blocks.value.length];
897
+ }
898
+ function onClipboard(event) {
899
+ if (event.defaultPrevented || paletteOpen.value) return;
900
+ if (isInForeignOverlay(event.target, workspaceElement())) return;
901
+ const action = event.type;
902
+ if (props.readonly && action !== "copy") return;
903
+ const path = selectedPath();
904
+ if (!clipboardActionApplies(action, clipboardTarget(event, path !== null))) return;
905
+ if (action === "paste") {
906
+ event.preventDefault();
907
+ void paste(...after(path), event.clipboardData?.getData("text/plain") ?? null);
908
+ } else if (path) {
909
+ if (action === "copy") copy(path, event);
910
+ else cut(path, event);
911
+ }
414
912
  }
415
- function duplicate(index) {
416
- if (!canAdd.value) return;
417
- setBlocks(duplicateBlock(blocks.value, index));
418
- select(index + 1);
913
+ function copyMenuItem(path) {
914
+ return {
915
+ label: "Copy",
916
+ icon: cmsIcon("clipboard-document"),
917
+ shortcut: "mod+c",
918
+ onSelect: () => copy(path)
919
+ };
419
920
  }
420
- function toggleHidden(index) {
421
- setBlocks(toggleBlockHidden(blocks.value, index, blockConfigAt(index)));
921
+ function nestedLists(path) {
922
+ const location = locate(path);
923
+ const fields = location?.block?.fields ?? {};
924
+ return Object.entries(fields).filter(
925
+ ([, field]) => field.type === "blocks" && isFieldVisible(field, location.item, fields)
926
+ ).map(([key, field]) => ({ key, label: field.label, list: [...path, key] }));
422
927
  }
423
- async function remove(index) {
424
- const label = blockConfigAt(index)?.label ?? "block";
425
- if (!await confirmAction(`Delete this ${label.toLowerCase()} block?`)) return;
426
- setBlocks(removeBlock(blocks.value, index));
427
- selected.value = selectionAfterRemove(selected.value, index);
928
+ function blockMenu(path) {
929
+ const location = locate(path);
930
+ if (!location) return [];
931
+ if (props.readonly) return [[copyMenuItem(path)]];
932
+ const list = blockListPath(path);
933
+ const index = blockPathIndex(path);
934
+ const full = canAddTo(list) ? void 0 : maxReachedIn(list);
935
+ const hidden = isHiddenBlock(location.item);
936
+ const length = listLength(list);
937
+ return [
938
+ [
939
+ {
940
+ label: "Add a block before",
941
+ icon: cmsIcon("bars-arrow-up"),
942
+ disabled: !!full,
943
+ disabledReason: full,
944
+ onSelect: () => openPalette(list, index)
945
+ },
946
+ {
947
+ label: "Add a block after",
948
+ icon: cmsIcon("bars-arrow-down"),
949
+ disabled: !!full,
950
+ disabledReason: full,
951
+ onSelect: () => openPalette(list, index + 1)
952
+ },
953
+ ...nestedLists(path).map((nested) => {
954
+ const reason = canAddTo(nested.list) ? void 0 : maxReachedIn(nested.list);
955
+ return {
956
+ label: `Add to ${nested.label}`,
957
+ icon: cmsIcon("plus"),
958
+ disabled: !!reason,
959
+ disabledReason: reason,
960
+ onSelect: () => openPalette(nested.list, listLength(nested.list))
961
+ };
962
+ })
963
+ ],
964
+ [
965
+ {
966
+ label: "Move up",
967
+ icon: cmsIcon("arrow-up"),
968
+ disabled: index === 0,
969
+ onSelect: () => moveBy(path, -1)
970
+ },
971
+ {
972
+ label: "Move down",
973
+ icon: cmsIcon("arrow-down"),
974
+ disabled: index === length - 1,
975
+ onSelect: () => moveBy(path, 1)
976
+ },
977
+ {
978
+ label: "Duplicate",
979
+ icon: cmsIcon("document-duplicate"),
980
+ disabled: !!full,
981
+ disabledReason: full,
982
+ onSelect: () => duplicate(path)
983
+ },
984
+ ...canHideBlock(location.block) ? [
985
+ {
986
+ label: hidden ? "Show" : "Hide",
987
+ icon: cmsIcon(hidden ? "eye" : "eye-slash"),
988
+ onSelect: () => toggleHidden(path)
989
+ }
990
+ ] : []
991
+ ],
992
+ [
993
+ copyMenuItem(path),
994
+ {
995
+ label: "Cut",
996
+ icon: cmsIcon("scissors"),
997
+ shortcut: "mod+x",
998
+ onSelect: () => cut(path)
999
+ },
1000
+ {
1001
+ label: "Paste after",
1002
+ icon: cmsIcon("clipboard"),
1003
+ shortcut: "mod+v",
1004
+ disabled: !!full,
1005
+ disabledReason: full,
1006
+ onSelect: () => void paste(list, index + 1)
1007
+ },
1008
+ ...presetsAllowed.value ? [
1009
+ {
1010
+ label: "Save as preset",
1011
+ icon: cmsIcon(CMS_PRESET_ICON),
1012
+ onSelect: () => openPresetSave(path)
1013
+ }
1014
+ ] : []
1015
+ ],
1016
+ [
1017
+ {
1018
+ label: "Delete",
1019
+ icon: cmsIcon("trash"),
1020
+ tone: "danger",
1021
+ onSelect: () => remove(path)
1022
+ }
1023
+ ]
1024
+ ];
428
1025
  }
429
1026
  function updateBlockField(key, value) {
430
- const index = selected.value;
431
- const current = index === null ? void 0 : blocks.value[index];
432
- if (index === null || !current) return;
433
- setBlocks(blocks.value.map((item, i) => i === index ? { ...current, [key]: value } : item));
1027
+ const path = selectedPath();
1028
+ if (!path) return;
1029
+ commitValues(
1030
+ updateBlockAt(state.value, path, (item) => ({ ...item, [key]: value })),
1031
+ `${formatBlockPath(path)}.${key}`
1032
+ );
1033
+ }
1034
+ const { canUndo, canRedo, change, release, undo, redo } = useCmsHistory({
1035
+ state: () => state.value,
1036
+ selected: () => selected.value,
1037
+ restore: (values, path) => {
1038
+ state.value = values;
1039
+ select(path);
1040
+ },
1041
+ omit: props.content ? ["status"] : []
1042
+ });
1043
+ function runShortcut(event) {
1044
+ if (event.defaultPrevented || event.isComposing || paletteOpen.value) return;
1045
+ if (isInForeignOverlay(event.target, workspaceElement())) return;
1046
+ const action = editorShortcut(event, onApplePlatform());
1047
+ const path = selectedPath();
1048
+ if (!action || !shortcutApplies(action, shortcutTarget(event, path !== null))) return;
1049
+ if (props.readonly && action !== "deselect") return;
1050
+ event.preventDefault();
1051
+ if (action === "undo") undo();
1052
+ else if (action === "redo") redo();
1053
+ else if (action === "save") emit("save");
1054
+ else if (path) runBlockShortcut(action, path);
1055
+ }
1056
+ function runBlockShortcut(action, path) {
1057
+ const list = blockListPath(path);
1058
+ const index = blockPathIndex(path);
1059
+ switch (action) {
1060
+ case "remove":
1061
+ remove(path);
1062
+ break;
1063
+ case "duplicate":
1064
+ duplicate(path);
1065
+ break;
1066
+ case "move-up":
1067
+ case "move-down":
1068
+ moveBy(path, action === "move-up" ? -1 : 1);
1069
+ break;
1070
+ case "insert-after":
1071
+ openPalette(list, index + 1);
1072
+ break;
1073
+ case "insert-before":
1074
+ openPalette(list, index);
1075
+ break;
1076
+ case "deselect":
1077
+ select(parentBlockPath(path));
1078
+ break;
1079
+ }
434
1080
  }
435
1081
  const media = computed(
436
1082
  () => Object.fromEntries(
@@ -440,6 +1086,8 @@ const media = computed(
440
1086
  ])
441
1087
  )
442
1088
  );
1089
+ const blocksFields = computed(() => ({ [props.fieldKey]: blocksField.value }));
1090
+ const { resolved: adminTheme } = useTheme();
443
1091
  const previewState = computed(() => ({
444
1092
  entry: props.entryName,
445
1093
  field: props.fieldKey,
@@ -447,38 +1095,97 @@ const previewState = computed(() => ({
447
1095
  blocks: blocks.value,
448
1096
  item: props.content ? state.value : null,
449
1097
  media: media.value,
450
- selected: selected.value
1098
+ selected: selected.value,
1099
+ editable: !props.readonly,
1100
+ theme: adminTheme.value,
1101
+ lockedFields: lockedInlineFields({
1102
+ fields: props.content ? props.fields : null,
1103
+ values: state.value,
1104
+ blocksFields: blocksFields.value,
1105
+ blocksValues: state.value,
1106
+ locked: props.locked
1107
+ })
451
1108
  }));
452
- function sendState() {
1109
+ const READONLY_FRAME_MESSAGES = /* @__PURE__ */ new Set(["ready", "select", "hover", "copy", "focus-field"]);
1110
+ function post(message) {
453
1111
  const target = frame.value?.contentWindow;
454
1112
  if (!target) return;
455
- const snapshot = JSON.parse(JSON.stringify(previewState.value));
456
- target.postMessage(previewEnvelope({ type: "render", state: snapshot }), window.location.origin);
1113
+ const plain = JSON.parse(JSON.stringify(message));
1114
+ target.postMessage(previewEnvelope(plain), window.location.origin);
1115
+ }
1116
+ function sendState() {
1117
+ post({ type: "render", state: previewState.value });
457
1118
  }
458
1119
  watch(previewState, sendState, { deep: true });
1120
+ function reloadPreview() {
1121
+ canvasHover.value = null;
1122
+ frame.value?.contentWindow?.location.reload();
1123
+ }
459
1124
  function onFrameMessage(message) {
1125
+ if (props.readonly && !READONLY_FRAME_MESSAGES.has(message.type)) return;
460
1126
  switch (message.type) {
461
1127
  case "ready":
462
1128
  sendState();
463
1129
  break;
464
1130
  case "select":
465
- select(message.index);
1131
+ paletteOpen.value = false;
1132
+ select(locate(message.path) ? message.path : null);
1133
+ break;
1134
+ case "hover":
1135
+ canvasHover.value = inField(message.path) ? message.path : null;
466
1136
  break;
467
1137
  case "insert":
468
- openPalette(message.index);
1138
+ if (inField(message.path))
1139
+ openPalette(
1140
+ blockListPath(message.path),
1141
+ blockPathIndex(message.path),
1142
+ insertButton(message.path)
1143
+ );
469
1144
  break;
470
- case "move":
471
- move(message.from, message.to);
472
- select(message.to);
1145
+ case "move": {
1146
+ const list = blockListPath(message.to);
1147
+ const index = blockPathIndex(message.to);
1148
+ const target = movedBlockPath(message.from, list, index);
1149
+ if (move(message.from, list, index) && target) select(target);
473
1150
  break;
1151
+ }
474
1152
  case "duplicate":
475
- duplicate(message.index);
1153
+ if (inField(message.path)) duplicate(message.path);
476
1154
  break;
477
1155
  case "toggle-hidden":
478
- toggleHidden(message.index);
1156
+ if (inField(message.path)) toggleHidden(message.path);
479
1157
  break;
480
1158
  case "remove":
481
- void remove(message.index);
1159
+ if (inField(message.path)) remove(message.path);
1160
+ break;
1161
+ case "copy":
1162
+ if (inField(message.path)) copy(message.path);
1163
+ break;
1164
+ case "cut":
1165
+ if (inField(message.path)) cut(message.path);
1166
+ break;
1167
+ case "paste":
1168
+ if (inField(message.path))
1169
+ void paste(
1170
+ blockListPath(message.path),
1171
+ blockPathIndex(message.path),
1172
+ message.text ?? null
1173
+ );
1174
+ break;
1175
+ case "focus-field":
1176
+ focusField(message.path, message.key);
1177
+ break;
1178
+ case "field-input":
1179
+ inlineInput(message);
1180
+ break;
1181
+ case "undo":
1182
+ undo();
1183
+ break;
1184
+ case "redo":
1185
+ redo();
1186
+ break;
1187
+ case "save":
1188
+ emit("save");
482
1189
  break;
483
1190
  }
484
1191
  }
@@ -495,45 +1202,135 @@ async function loadLibrary() {
495
1202
  library.value = /* @__PURE__ */ new Map();
496
1203
  }
497
1204
  }
498
- const panel = ref(null);
499
- const { highlighted, reveal } = useCmsFieldFocus();
1205
+ const { highlighted, highlight: highlightField } = useFieldHighlight();
1206
+ function workspaceElement() {
1207
+ const element = workspace.value?.$el;
1208
+ return element instanceof Element ? element : null;
1209
+ }
1210
+ function revealBlock(path) {
1211
+ select(path);
1212
+ if (!selected.value) {
1213
+ showPanel("left");
1214
+ return;
1215
+ }
1216
+ showPanel("right");
1217
+ post({ type: "reveal", path: selected.value });
1218
+ }
1219
+ function revealField(key) {
1220
+ select(null);
1221
+ activeTab.value = tabOfKey(key) === "seo" ? "seo" : "content";
1222
+ showPanel("right");
1223
+ }
1224
+ function fieldAt(path, key) {
1225
+ if (path && !inField(path)) return void 0;
1226
+ return previewFieldConfig(
1227
+ { path, key },
1228
+ blocksFields.value,
1229
+ state.value,
1230
+ props.content ? props.fields : null,
1231
+ props.fieldKey
1232
+ );
1233
+ }
1234
+ function focusField(path, key) {
1235
+ if (!fieldAt(path, key)) return;
1236
+ select(path);
1237
+ if (!path) activeTab.value = tabOfKey(key) === "seo" ? "seo" : "content";
1238
+ if (!isCompact()) inspectorCollapsed.value = false;
1239
+ revealInPanel(key);
1240
+ }
1241
+ function inlineInput(message) {
1242
+ if (!applyInlineInput(message)) sendState();
1243
+ }
1244
+ function applyInlineInput(message) {
1245
+ const { path, key, locale: code } = message;
1246
+ const declared = fieldAt(path, key);
1247
+ if (props.readonly || !isInlineTextField(declared)) return false;
1248
+ if (previewState.value.lockedFields.includes(inlineFieldPath(path, key))) return false;
1249
+ const localized = isTranslatableField(declared) && i18n.locales.length > 0;
1250
+ if (localized && !i18n.locales.includes(code)) return false;
1251
+ const field = localized ? declared : { ...declared, translatable: false };
1252
+ const item = path ? blockAt(state.value, path) : null;
1253
+ if (path && !item) return false;
1254
+ const text = clampInlineText(
1255
+ normalizeInlineText(message.value, !!field.textarea),
1256
+ inlineTextLimit(field)
1257
+ );
1258
+ const value = inlineFieldValue(
1259
+ field,
1260
+ item ? item[key] : state.value[key],
1261
+ code,
1262
+ i18n.defaultLocale,
1263
+ text
1264
+ );
1265
+ const coalesce = `inline:${path ? formatBlockPath(path) : ""}.${key}`;
1266
+ if (!path) setValue(key, value, coalesce, true);
1267
+ else
1268
+ commitValues(
1269
+ updateBlockAt(state.value, path, (current) => ({ ...current, [key]: value })),
1270
+ coalesce,
1271
+ true
1272
+ );
1273
+ if (message.commit) void nextTick(release);
1274
+ return true;
1275
+ }
500
1276
  function applyFocus(focus) {
501
1277
  if (focus.locale && i18n.locales.includes(focus.locale)) locale.value = focus.locale;
502
1278
  const key = focus.field;
503
1279
  if (!key || !Object.hasOwn(props.fields, key)) return;
504
1280
  if (key === props.fieldKey) {
505
- activeTab.value = "block";
506
- if (focus.block != null && focus.block < blocks.value.length) select(focus.block);
1281
+ const path = focus.blockPath ?? (focus.block != null ? [key, focus.block] : null);
1282
+ revealBlock(locate(path) ? path : null);
507
1283
  return;
508
1284
  }
509
- activeTab.value = tabOfKey(key);
510
- reveal(() => panel.value?.el, key);
1285
+ revealField(key);
1286
+ revealInPanel(key);
511
1287
  }
1288
+ let frameObserver = null;
512
1289
  onMounted(() => {
513
1290
  window.addEventListener("message", onMessage);
1291
+ document.addEventListener("keydown", runShortcut);
1292
+ document.addEventListener("copy", onClipboard);
1293
+ document.addEventListener("cut", onClipboard);
1294
+ document.addEventListener("paste", onClipboard);
1295
+ frameObserver = new ResizeObserver(([entry]) => {
1296
+ canvasWidth.value = entry ? Math.round(entry.contentRect.width) : null;
1297
+ });
1298
+ watch(
1299
+ frameBox,
1300
+ (element, previous) => {
1301
+ if (previous) frameObserver?.unobserve(previous);
1302
+ if (element) frameObserver?.observe(element);
1303
+ },
1304
+ { immediate: true }
1305
+ );
514
1306
  void loadLibrary();
1307
+ void loadPresets();
515
1308
  if (props.focus) applyFocus(props.focus);
516
1309
  });
517
1310
  onBeforeUnmount(() => {
518
1311
  window.removeEventListener("message", onMessage);
1312
+ document.removeEventListener("keydown", runShortcut);
1313
+ document.removeEventListener("copy", onClipboard);
1314
+ document.removeEventListener("cut", onClipboard);
1315
+ document.removeEventListener("paste", onClipboard);
1316
+ frameObserver?.disconnect();
519
1317
  });
520
1318
  const validateHook = computed(
521
1319
  () => props.content ? cmsConfig[props.entryName]?.validate : void 0
522
1320
  );
523
1321
  const schema = computed(
524
1322
  () => buildEntrySchema(
525
- { fields: props.fields, drafts: props.content, validate: validateHook.value },
1323
+ {
1324
+ fields: props.fields,
1325
+ drafts: props.content,
1326
+ validate: validateHook.value,
1327
+ config: cmsConfig
1328
+ },
526
1329
  i18n
527
1330
  )
528
1331
  );
529
1332
  function issueKey(path) {
530
- const [key, index, blockKey] = path;
531
- const keys = [String(key)];
532
- if (String(key) === props.fieldKey && typeof index === "number")
533
- keys.push(
534
- blockKey === void 0 ? `${String(key)}.${index}` : `${String(key)}.${index}.${String(blockKey)}`
535
- );
536
- return keys;
1333
+ return editorIssueKeys(props.fields, state.value, path, props.fieldKey);
537
1334
  }
538
1335
  const form = useForm({
539
1336
  values: state,
@@ -541,8 +1338,15 @@ const form = useForm({
541
1338
  warnings: (values) => entryValidationWarnings({ fields: props.fields, validate: validateHook.value }, values, i18n),
542
1339
  issueKey
543
1340
  });
1341
+ const blockErrors = computed(() => blockErrorPaths(form.errors, props.fieldKey));
1342
+ const topLevelErrorCount = computed(
1343
+ () => [...blockErrors.value].filter((path) => path.split(".").length === 2).length
1344
+ );
1345
+ const blocksFieldError = computed(
1346
+ () => blockErrors.value.size ? void 0 : form.errors[props.fieldKey]
1347
+ );
544
1348
  function tabOfKey(key) {
545
- if (key === props.fieldKey) return "block";
1349
+ if (key === props.fieldKey) return "blocks";
546
1350
  return SEO_KEYS.includes(key) ? "seo" : "content";
547
1351
  }
548
1352
  function tabErrorCount(tab) {
@@ -551,35 +1355,46 @@ function tabErrorCount(tab) {
551
1355
  );
552
1356
  return keys.size;
553
1357
  }
554
- function firstError() {
1358
+ function revealFirstError() {
555
1359
  const keys = Object.keys(form.errors).filter((key) => form.errors[key]);
556
1360
  const name = Object.keys(props.fields).find(
557
- (field) => keys.some((key) => key === field || key.startsWith(`${field}.`))
1361
+ (field2) => keys.some((key) => key === field2 || key.startsWith(`${field2}.`))
558
1362
  );
559
- if (!name) return void 0;
560
- const indexes = keys.filter((key) => key.startsWith(`${name}.`)).map((key) => Number(key.split(".")[1])).filter(Number.isInteger);
561
- return { name, index: indexes.length ? Math.min(...indexes) : null };
1363
+ if (!name) return;
1364
+ const field = props.fields[name];
1365
+ if (name === props.fieldKey) {
1366
+ const first = firstBlockError(form.errors, props.fieldKey);
1367
+ revealBlock(first?.path ?? null);
1368
+ if (first?.key) revealInPanel(first.key);
1369
+ } else if (field) {
1370
+ revealField(name);
1371
+ expandSection(name);
1372
+ if (isTranslatableField(field)) locale.value = i18n.defaultLocale;
1373
+ }
562
1374
  }
563
1375
  function validate() {
564
1376
  form.submitCount.value++;
565
1377
  if (form.validateSync()) return true;
566
- const first = firstError();
567
- if (!first) return false;
568
- const field = props.fields[first.name];
569
- if (first.name === props.fieldKey) {
570
- select(first.index);
571
- activeTab.value = "block";
572
- } else if (field) {
573
- activeTab.value = tabOfKey(first.name);
574
- if (isTranslatableField(field)) locale.value = i18n.defaultLocale;
575
- }
1378
+ revealFirstError();
576
1379
  return false;
577
1380
  }
578
- watch(
579
- () => blocks.value.length,
580
- (length) => {
581
- if (selected.value !== null && selected.value >= length) selected.value = null;
1381
+ function setIssues(issues) {
1382
+ const fieldIssues = issues.filter((issue) => issue.path.length);
1383
+ if (!fieldIssues.length) return;
1384
+ form.setIssues(fieldIssues);
1385
+ revealFirstError();
1386
+ }
1387
+ watch(state, (values) => {
1388
+ if (values[props.fieldKey] !== ownBlocks) {
1389
+ ownBlocks = values[props.fieldKey];
1390
+ dropInsertionPoints();
582
1391
  }
583
- );
584
- defineExpose({ validate, locale });
1392
+ if (selected.value && !blockAt(values, selected.value)) selected.value = null;
1393
+ });
1394
+ function applyChange(values) {
1395
+ if (props.readonly) return;
1396
+ change();
1397
+ state.value = values;
1398
+ }
1399
+ defineExpose({ validate, setIssues, applyChange, locale });
585
1400
  </script>