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