@michaelthielemann/kestrel 1.2.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 (395) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +12 -0
  3. package/README.md +234 -0
  4. package/kestrel.config.ts +28 -0
  5. package/layers/access/nuxt.config.ts +5 -0
  6. package/layers/access/server/middleware/00.ip-allowlist.ts +67 -0
  7. package/layers/access/server/middleware/access-guard.ts +30 -0
  8. package/layers/access/server/types.d.ts +8 -0
  9. package/layers/access/server/utils/csrf.ts +39 -0
  10. package/layers/access/server/utils/grant-registry.ts +36 -0
  11. package/layers/access/server/utils/guard.ts +44 -0
  12. package/layers/access/server/utils/ip-allowlist.ts +76 -0
  13. package/layers/access/server/utils/policy.ts +98 -0
  14. package/layers/access/server/utils/public-resources.ts +11 -0
  15. package/layers/access/server/utils/render-context.ts +41 -0
  16. package/layers/access/server/utils/require-admin.ts +15 -0
  17. package/layers/admin/app/components/AdminAccount.vue +212 -0
  18. package/layers/admin/app/components/AdminNav.vue +44 -0
  19. package/layers/admin/app/components/BlockFields.vue +68 -0
  20. package/layers/admin/app/components/BlockPreview.vue +401 -0
  21. package/layers/admin/app/components/BlockTree.vue +433 -0
  22. package/layers/admin/app/components/BlocksBody.vue +162 -0
  23. package/layers/admin/app/components/CollectionDeleteDialog.vue +38 -0
  24. package/layers/admin/app/components/CollectionEditor.vue +203 -0
  25. package/layers/admin/app/components/CollectionList.vue +1091 -0
  26. package/layers/admin/app/components/EditorStatus.vue +199 -0
  27. package/layers/admin/app/components/EditorUnsupported.vue +28 -0
  28. package/layers/admin/app/components/FieldsBody.vue +23 -0
  29. package/layers/admin/app/components/LocaleBar.vue +105 -0
  30. package/layers/admin/app/components/PageFields.vue +101 -0
  31. package/layers/admin/app/components/PageFieldsPane.vue +39 -0
  32. package/layers/admin/app/components/SeoFields.vue +169 -0
  33. package/layers/admin/app/components/SingletonEditor.vue +92 -0
  34. package/layers/admin/app/composables/useAuth.ts +68 -0
  35. package/layers/admin/app/composables/useBlockTree.ts +95 -0
  36. package/layers/admin/app/composables/useBlocks.ts +7 -0
  37. package/layers/admin/app/composables/useCachedResource.ts +35 -0
  38. package/layers/admin/app/composables/useCollectionOps.ts +53 -0
  39. package/layers/admin/app/composables/useCollections.ts +7 -0
  40. package/layers/admin/app/composables/useContentLocales.ts +23 -0
  41. package/layers/admin/app/composables/useEditForm.ts +370 -0
  42. package/layers/admin/app/composables/useLinkResolver.ts +40 -0
  43. package/layers/admin/app/composables/useListColumns.ts +53 -0
  44. package/layers/admin/app/composables/useListUrlState.ts +53 -0
  45. package/layers/admin/app/composables/usePendingPublishPoll.ts +15 -0
  46. package/layers/admin/app/composables/usePreviewViewport.ts +30 -0
  47. package/layers/admin/app/composables/usePublishStatus.ts +98 -0
  48. package/layers/admin/app/composables/useRailCollapsed.ts +14 -0
  49. package/layers/admin/app/composables/useTheme.ts +36 -0
  50. package/layers/admin/app/composables/useUnsavedGuard.ts +22 -0
  51. package/layers/admin/app/layouts/admin.vue +267 -0
  52. package/layers/admin/app/middleware/admin-auth.ts +4 -0
  53. package/layers/admin/app/pages/admin/[collection]/[id].vue +202 -0
  54. package/layers/admin/app/pages/admin/[collection]/index.vue +62 -0
  55. package/layers/admin/app/pages/admin/index.vue +118 -0
  56. package/layers/admin/app/pages/admin/login.vue +68 -0
  57. package/layers/admin/app/pages/admin/media.vue +28 -0
  58. package/layers/admin/app/pages/admin/references.vue +146 -0
  59. package/layers/admin/app/plugins/admin-style-guard.client.ts +76 -0
  60. package/layers/admin/app/plugins/reauth.client.ts +20 -0
  61. package/layers/admin/app/utils/admin-style-guard.ts +20 -0
  62. package/layers/admin/app/utils/block-tree.ts +214 -0
  63. package/layers/admin/app/utils/collection-ops.ts +44 -0
  64. package/layers/admin/app/utils/dead-refs.ts +30 -0
  65. package/layers/admin/app/utils/edit-form.ts +223 -0
  66. package/layers/admin/app/utils/editor-expose.ts +26 -0
  67. package/layers/admin/app/utils/editor-form-context.ts +83 -0
  68. package/layers/admin/app/utils/editor-registry.ts +20 -0
  69. package/layers/admin/app/utils/field-keys.ts +11 -0
  70. package/layers/admin/app/utils/list-columns.ts +141 -0
  71. package/layers/admin/app/utils/list-query.ts +96 -0
  72. package/layers/admin/app/utils/nav-active.ts +9 -0
  73. package/layers/admin/app/utils/populate-blocks.ts +288 -0
  74. package/layers/admin/app/utils/preview-channel.ts +62 -0
  75. package/layers/admin/app/utils/preview-viewport.ts +80 -0
  76. package/layers/admin/app/utils/reauth.ts +68 -0
  77. package/layers/admin/app/utils/record-title.ts +15 -0
  78. package/layers/admin/app/utils/register-builtin-editors.ts +9 -0
  79. package/layers/admin/app/utils/safe-redirect.ts +12 -0
  80. package/layers/admin/nuxt.config.ts +17 -0
  81. package/layers/auth/nuxt.config.ts +1 -0
  82. package/layers/auth/server/api/auth/login.post.ts +38 -0
  83. package/layers/auth/server/api/auth/logout.post.ts +9 -0
  84. package/layers/auth/server/api/auth/session.get.ts +1 -0
  85. package/layers/auth/server/utils/admin-credential.ts +42 -0
  86. package/layers/auth/server/utils/client-ip.ts +88 -0
  87. package/layers/auth/server/utils/login-throttle.ts +126 -0
  88. package/layers/auth/server/utils/password.ts +52 -0
  89. package/layers/auth/server/utils/session-cookie.ts +44 -0
  90. package/layers/auth/server/utils/session-epoch.ts +96 -0
  91. package/layers/auth/server/utils/session.ts +85 -0
  92. package/layers/collections/nuxt.config.ts +1 -0
  93. package/layers/collections/server/collections/pages.ts +19 -0
  94. package/layers/collections/server/plugins/01.register.ts +32 -0
  95. package/layers/collections/server/plugins/02.register-relation-populate.ts +17 -0
  96. package/layers/collections/server/utils/populate-relations.ts +66 -0
  97. package/layers/core/app/utils/bulk-actions.ts +9 -0
  98. package/layers/core/app/utils/filter-ops.ts +98 -0
  99. package/layers/core/app/utils/list-limits.ts +27 -0
  100. package/layers/core/app/utils/locale-path.ts +12 -0
  101. package/layers/core/app/utils/slugify.ts +16 -0
  102. package/layers/core/modules/auto-discovery/extract-block.ts +176 -0
  103. package/layers/core/modules/auto-discovery/index.ts +44 -0
  104. package/layers/core/modules/auto-discovery/scan.ts +78 -0
  105. package/layers/core/modules/auto-discovery/virtual.d.ts +12 -0
  106. package/layers/core/modules/kestrel/index.ts +86 -0
  107. package/layers/core/nuxt.config.ts +8 -0
  108. package/layers/core/server/api/[collection]/[id]/dead-refs.get.ts +8 -0
  109. package/layers/core/server/api/[collection]/[id]/translations.get.ts +4 -0
  110. package/layers/core/server/api/[collection]/[id].delete.ts +5 -0
  111. package/layers/core/server/api/[collection]/[id].get.ts +8 -0
  112. package/layers/core/server/api/[collection]/[id].patch.ts +8 -0
  113. package/layers/core/server/api/[collection]/bulk.post.ts +37 -0
  114. package/layers/core/server/api/[collection]/index.get.ts +19 -0
  115. package/layers/core/server/api/[collection]/index.post.ts +8 -0
  116. package/layers/core/server/api/[collection]/index.put.ts +7 -0
  117. package/layers/core/server/api/[collection]/options.get.ts +18 -0
  118. package/layers/core/server/api/[collection]/translations.get.ts +25 -0
  119. package/layers/core/server/api/collections/[name].get.ts +4 -0
  120. package/layers/core/server/api/collections/index.get.ts +3 -0
  121. package/layers/core/server/api/references/broken.get.ts +3 -0
  122. package/layers/core/server/api/references/referrers.get.ts +24 -0
  123. package/layers/core/server/database/record-refs.ts +20 -0
  124. package/layers/core/server/plugins/00.migrate.ts +11 -0
  125. package/layers/core/server/plugins/02.schema-sync.ts +63 -0
  126. package/layers/core/server/plugins/03.record-refs.ts +12 -0
  127. package/layers/core/server/schema/bootstrap.ts +41 -0
  128. package/layers/core/server/schema/desired.ts +99 -0
  129. package/layers/core/server/schema/dev-sync.ts +23 -0
  130. package/layers/core/server/schema/dialect.ts +59 -0
  131. package/layers/core/server/schema/diff.ts +110 -0
  132. package/layers/core/server/schema/introspect.ts +67 -0
  133. package/layers/core/server/schema/model.ts +61 -0
  134. package/layers/core/server/schema/render-sqlite.ts +89 -0
  135. package/layers/core/server/schema/sync.ts +201 -0
  136. package/layers/core/server/tasks/db/migrate.ts +54 -0
  137. package/layers/core/server/utils/blocks.ts +6 -0
  138. package/layers/core/server/utils/collection-types.ts +27 -0
  139. package/layers/core/server/utils/crud.ts +577 -0
  140. package/layers/core/server/utils/db.ts +26 -0
  141. package/layers/core/server/utils/defineCollection.ts +207 -0
  142. package/layers/core/server/utils/duplicate.ts +162 -0
  143. package/layers/core/server/utils/field-layout.ts +125 -0
  144. package/layers/core/server/utils/filter-predicate.ts +101 -0
  145. package/layers/core/server/utils/http.ts +50 -0
  146. package/layers/core/server/utils/if-unmodified.ts +13 -0
  147. package/layers/core/server/utils/kestrel-config.ts +410 -0
  148. package/layers/core/server/utils/key-lock.ts +33 -0
  149. package/layers/core/server/utils/locale.ts +53 -0
  150. package/layers/core/server/utils/page-route.ts +58 -0
  151. package/layers/core/server/utils/page-slug.ts +127 -0
  152. package/layers/core/server/utils/picker.ts +82 -0
  153. package/layers/core/server/utils/populate.ts +48 -0
  154. package/layers/core/server/utils/prerender-memo.ts +23 -0
  155. package/layers/core/server/utils/read-capture.ts +37 -0
  156. package/layers/core/server/utils/record-ref-index.ts +200 -0
  157. package/layers/core/server/utils/registry.ts +24 -0
  158. package/layers/core/server/utils/resolve-scope.ts +79 -0
  159. package/layers/core/server/utils/seo.ts +24 -0
  160. package/layers/core/server/utils/serialize-collection.ts +149 -0
  161. package/layers/core/server/utils/server-config.ts +18 -0
  162. package/layers/core/server/utils/sql.ts +2 -0
  163. package/layers/core/server/utils/storage.local.ts +117 -0
  164. package/layers/core/server/utils/storage.s3.ts +197 -0
  165. package/layers/core/server/utils/storage.ts +90 -0
  166. package/layers/core/server/utils/write-events.ts +31 -0
  167. package/layers/fields/app/utils/condition.ts +71 -0
  168. package/layers/fields/app/utils/define-block.ts +13 -0
  169. package/layers/fields/app/utils/field-constraints.ts +17 -0
  170. package/layers/fields/app/utils/field-factories.ts +129 -0
  171. package/layers/fields/app/utils/field-meta.ts +33 -0
  172. package/layers/fields/app/utils/field-validate.ts +75 -0
  173. package/layers/fields/app/utils/field-value.ts +7 -0
  174. package/layers/fields/app/utils/richtext-links.ts +48 -0
  175. package/layers/fields/nuxt.config.ts +1 -0
  176. package/layers/fields/server/api/blocks/index.get.ts +9 -0
  177. package/layers/fields/server/field-registry/index.ts +248 -0
  178. package/layers/fields/server/field-registry/naming.ts +10 -0
  179. package/layers/fields/server/field-registry/sanitize.ts +40 -0
  180. package/layers/fields/server/field-registry/types.ts +19 -0
  181. package/layers/fields/server/plugins/01.register-field-populate.ts +11 -0
  182. package/layers/fields/server/utils/block-ids.ts +43 -0
  183. package/layers/fields/server/utils/block-populate.ts +41 -0
  184. package/layers/fields/server/utils/buildCollection.ts +88 -0
  185. package/layers/fields/server/utils/buildFieldSchema.ts +22 -0
  186. package/layers/fields/server/utils/buildTable.ts +107 -0
  187. package/layers/fields/server/utils/conditional-required.ts +25 -0
  188. package/layers/fields/server/utils/defineBlock.ts +86 -0
  189. package/layers/fields/server/utils/defineFieldType.ts +33 -0
  190. package/layers/fields/server/utils/extract-refs.ts +152 -0
  191. package/layers/fields/server/utils/field-populate.ts +68 -0
  192. package/layers/media/app/components/KestrelImg.vue +65 -0
  193. package/layers/media/app/components/MediaConflictDialog.vue +35 -0
  194. package/layers/media/app/components/MediaDeleteDialog.vue +52 -0
  195. package/layers/media/app/components/MediaGrid.vue +113 -0
  196. package/layers/media/app/components/MediaImage.vue +70 -0
  197. package/layers/media/app/components/MediaLibrary.vue +451 -0
  198. package/layers/media/app/components/MediaNewFolderDialog.vue +32 -0
  199. package/layers/media/app/components/MediaPathBar.vue +163 -0
  200. package/layers/media/app/components/MediaPicker.vue +19 -0
  201. package/layers/media/app/components/MediaRenameDialog.vue +32 -0
  202. package/layers/media/app/components/MediaTable.vue +75 -0
  203. package/layers/media/app/components/MediaThumb.vue +25 -0
  204. package/layers/media/app/components/MediaToolbar.vue +90 -0
  205. package/layers/media/app/components/MediaUploadDialog.vue +87 -0
  206. package/layers/media/app/components/MediaViewer.vue +88 -0
  207. package/layers/media/app/components/field/Media.vue +261 -0
  208. package/layers/media/app/composables/useBlurUp.ts +55 -0
  209. package/layers/media/app/composables/useMediaClipboard.ts +14 -0
  210. package/layers/media/app/composables/useMediaContextMenu.ts +56 -0
  211. package/layers/media/app/composables/useMediaDnd.ts +56 -0
  212. package/layers/media/app/composables/useMediaLibrary.ts +151 -0
  213. package/layers/media/app/composables/useMediaOps.ts +50 -0
  214. package/layers/media/app/composables/useMediaRelocate.ts +39 -0
  215. package/layers/media/app/composables/useMediaResolver.ts +50 -0
  216. package/layers/media/app/composables/useMediaUpload.ts +93 -0
  217. package/layers/media/app/composables/useMediaVariant.ts +34 -0
  218. package/layers/media/app/plugins/field-media.client.ts +6 -0
  219. package/layers/media/app/utils/dnd.ts +69 -0
  220. package/layers/media/app/utils/library.ts +79 -0
  221. package/layers/media/app/utils/ops.ts +62 -0
  222. package/layers/media/app/utils/picture-model.ts +125 -0
  223. package/layers/media/app/utils/thumbhash.ts +19 -0
  224. package/layers/media/app/utils/variant-name.ts +20 -0
  225. package/layers/media/nuxt.config.ts +3 -0
  226. package/layers/media/server/api/media/[id]/usages.get.ts +8 -0
  227. package/layers/media/server/api/media/[id].delete.ts +18 -0
  228. package/layers/media/server/api/media/[id].get.ts +10 -0
  229. package/layers/media/server/api/media/[id].patch.ts +41 -0
  230. package/layers/media/server/api/media/backfill.post.ts +18 -0
  231. package/layers/media/server/api/media/copy.post.ts +7 -0
  232. package/layers/media/server/api/media/delete.post.ts +13 -0
  233. package/layers/media/server/api/media/folders.post.ts +18 -0
  234. package/layers/media/server/api/media/index.get.ts +15 -0
  235. package/layers/media/server/api/media/index.post.ts +157 -0
  236. package/layers/media/server/api/media/library.get.ts +17 -0
  237. package/layers/media/server/api/media/move.post.ts +7 -0
  238. package/layers/media/server/api/media/rename.post.ts +7 -0
  239. package/layers/media/server/api/media/resolve.get.ts +13 -0
  240. package/layers/media/server/collections/media-settings.ts +25 -0
  241. package/layers/media/server/collections/media.ts +29 -0
  242. package/layers/media/server/database/folders.ts +10 -0
  243. package/layers/media/server/middleware/ondemand-variants.ts +37 -0
  244. package/layers/media/server/plugins/02.register-media.ts +19 -0
  245. package/layers/media/server/plugins/03.media-cleanup.ts +20 -0
  246. package/layers/media/server/plugins/04.variant-capture.ts +23 -0
  247. package/layers/media/server/schema-tables/folders.ts +4 -0
  248. package/layers/media/server/tasks/media/backfill.ts +18 -0
  249. package/layers/media/server/utils/backfill.ts +132 -0
  250. package/layers/media/server/utils/derive.ts +59 -0
  251. package/layers/media/server/utils/folder-paths.ts +42 -0
  252. package/layers/media/server/utils/folders.ts +12 -0
  253. package/layers/media/server/utils/library.ts +90 -0
  254. package/layers/media/server/utils/media-cleanup.ts +27 -0
  255. package/layers/media/server/utils/media-enabled.ts +23 -0
  256. package/layers/media/server/utils/media-ops.ts +118 -0
  257. package/layers/media/server/utils/media-write.ts +13 -0
  258. package/layers/media/server/utils/naming.ts +52 -0
  259. package/layers/media/server/utils/ondemand.ts +69 -0
  260. package/layers/media/server/utils/persist-upload.ts +62 -0
  261. package/layers/media/server/utils/populate.ts +47 -0
  262. package/layers/media/server/utils/record.ts +63 -0
  263. package/layers/media/server/utils/relocate-ops.ts +341 -0
  264. package/layers/media/server/utils/relocate-plan.ts +63 -0
  265. package/layers/media/server/utils/resolve.ts +86 -0
  266. package/layers/media/server/utils/sanitize-svg.ts +12 -0
  267. package/layers/media/server/utils/sniff.ts +68 -0
  268. package/layers/media/server/utils/storage-relocate.ts +139 -0
  269. package/layers/media/server/utils/translations.ts +19 -0
  270. package/layers/media/server/utils/usages.ts +66 -0
  271. package/layers/media/server/utils/variant-capture.ts +70 -0
  272. package/layers/media/server/utils/variants.ts +108 -0
  273. package/layers/public/app/app.vue +5 -0
  274. package/layers/public/app/components/BlockRenderer.vue +56 -0
  275. package/layers/public/app/components/KestrelLink.vue +26 -0
  276. package/layers/public/app/components/KestrelPreviewBridge.vue +133 -0
  277. package/layers/public/app/composables/public-page.ts +24 -0
  278. package/layers/public/app/error.vue +12 -0
  279. package/layers/public/app/layouts/default.vue +5 -0
  280. package/layers/public/app/pages/[...slug].vue +157 -0
  281. package/layers/public/app/pages/__kestrel/preview.vue +34 -0
  282. package/layers/public/app/utils/block-edit-context.ts +16 -0
  283. package/layers/public/app/utils/link-href.ts +46 -0
  284. package/layers/public/app/utils/page-head.ts +84 -0
  285. package/layers/public/app/utils/preview-protocol.ts +81 -0
  286. package/layers/public/app/utils/public-route.ts +46 -0
  287. package/layers/public/modules/deploy-output/deploy-output.ts +370 -0
  288. package/layers/public/modules/deploy-output/index.ts +82 -0
  289. package/layers/public/modules/prerender-routes/discover.ts +37 -0
  290. package/layers/public/modules/prerender-routes/index.ts +85 -0
  291. package/layers/public/modules/prerender-routes/routes.ts +16 -0
  292. package/layers/public/modules/prune-media/index.ts +48 -0
  293. package/layers/public/modules/prune-media/prune-media.ts +81 -0
  294. package/layers/public/nuxt.config.ts +11 -0
  295. package/layers/public/server/api/links/resolve.get.ts +19 -0
  296. package/layers/public/server/api/publish-status.get.ts +44 -0
  297. package/layers/public/server/api/route.get.ts +18 -0
  298. package/layers/public/server/database/publish-deps.ts +12 -0
  299. package/layers/public/server/database/publish-status.ts +19 -0
  300. package/layers/public/server/plugins/02.register-links.ts +17 -0
  301. package/layers/public/server/plugins/zz.publish.ts +63 -0
  302. package/layers/public/server/routes/llms.txt.get.ts +72 -0
  303. package/layers/public/server/routes/robots.txt.get.ts +8 -0
  304. package/layers/public/server/routes/sitemap.xml.get.ts +66 -0
  305. package/layers/public/server/schema-tables/publish-deps.ts +4 -0
  306. package/layers/public/server/schema-tables/publish-status.ts +4 -0
  307. package/layers/public/server/tasks/publish/run.ts +26 -0
  308. package/layers/public/server/utils/link-resolve.ts +35 -0
  309. package/layers/public/server/utils/llms.ts +39 -0
  310. package/layers/public/server/utils/page-resolve.ts +86 -0
  311. package/layers/public/server/utils/populate-links.ts +66 -0
  312. package/layers/public/server/utils/publish/coalesce.ts +22 -0
  313. package/layers/public/server/utils/publish/deps-persistence.ts +43 -0
  314. package/layers/public/server/utils/publish/deps.ts +87 -0
  315. package/layers/public/server/utils/publish/invalidation.ts +141 -0
  316. package/layers/public/server/utils/publish/publish-status.ts +54 -0
  317. package/layers/public/server/utils/publish/publisher.ts +291 -0
  318. package/layers/public/server/utils/publish/queue.ts +67 -0
  319. package/layers/public/server/utils/publish/route-for-record.ts +16 -0
  320. package/layers/public/server/utils/publish/route-keys.ts +13 -0
  321. package/layers/public/server/utils/site-url.ts +32 -0
  322. package/layers/public/server/utils/sitemap.ts +97 -0
  323. package/layers/ui/app/assets/scss/_base.scss +16 -0
  324. package/layers/ui/app/assets/scss/_datepicker.scss +82 -0
  325. package/layers/ui/app/assets/scss/_mixins.scss +50 -0
  326. package/layers/ui/app/assets/scss/_reset.scss +65 -0
  327. package/layers/ui/app/assets/scss/_tokens.scss +183 -0
  328. package/layers/ui/app/assets/scss/main.scss +4 -0
  329. package/layers/ui/app/components/field/Boolean.vue +30 -0
  330. package/layers/ui/app/components/field/Choice.vue +68 -0
  331. package/layers/ui/app/components/field/Datetime.vue +96 -0
  332. package/layers/ui/app/components/field/DeadRefNote.vue +28 -0
  333. package/layers/ui/app/components/field/Json.vue +63 -0
  334. package/layers/ui/app/components/field/Layout.vue +140 -0
  335. package/layers/ui/app/components/field/Link.vue +112 -0
  336. package/layers/ui/app/components/field/LinkInternalPicker.vue +95 -0
  337. package/layers/ui/app/components/field/Number.vue +30 -0
  338. package/layers/ui/app/components/field/Relation.vue +49 -0
  339. package/layers/ui/app/components/field/Renderer.vue +30 -0
  340. package/layers/ui/app/components/field/Repeater.vue +460 -0
  341. package/layers/ui/app/components/field/Richtext.vue +26 -0
  342. package/layers/ui/app/components/field/Slug.vue +66 -0
  343. package/layers/ui/app/components/field/Text.vue +35 -0
  344. package/layers/ui/app/components/field/Unsupported.vue +34 -0
  345. package/layers/ui/app/components/ui/Alert.vue +50 -0
  346. package/layers/ui/app/components/ui/Brand.vue +22 -0
  347. package/layers/ui/app/components/ui/Button.vue +116 -0
  348. package/layers/ui/app/components/ui/ButtonGroup.vue +96 -0
  349. package/layers/ui/app/components/ui/Checkbox.vue +52 -0
  350. package/layers/ui/app/components/ui/CheckboxGroup.vue +50 -0
  351. package/layers/ui/app/components/ui/Combobox.vue +343 -0
  352. package/layers/ui/app/components/ui/DatePicker.vue +92 -0
  353. package/layers/ui/app/components/ui/DateRangePicker.vue +100 -0
  354. package/layers/ui/app/components/ui/Dialog.vue +107 -0
  355. package/layers/ui/app/components/ui/EmptyState.vue +46 -0
  356. package/layers/ui/app/components/ui/Field.vue +57 -0
  357. package/layers/ui/app/components/ui/Fieldset.vue +64 -0
  358. package/layers/ui/app/components/ui/Icon.vue +44 -0
  359. package/layers/ui/app/components/ui/Menu.vue +54 -0
  360. package/layers/ui/app/components/ui/NumberInput.vue +173 -0
  361. package/layers/ui/app/components/ui/Richtext.vue +119 -0
  362. package/layers/ui/app/components/ui/RichtextToolbar.vue +253 -0
  363. package/layers/ui/app/components/ui/Select.vue +51 -0
  364. package/layers/ui/app/components/ui/TextInput.vue +110 -0
  365. package/layers/ui/app/components/ui/Textarea.vue +38 -0
  366. package/layers/ui/app/components/ui/TimeInput.vue +17 -0
  367. package/layers/ui/app/components/ui/Toasts.vue +103 -0
  368. package/layers/ui/app/components/ui/Tooltip.vue +45 -0
  369. package/layers/ui/app/composables/useAdminLang.ts +8 -0
  370. package/layers/ui/app/composables/useDragReorder.ts +41 -0
  371. package/layers/ui/app/composables/useEchoGuard.ts +28 -0
  372. package/layers/ui/app/composables/useLinkField.ts +70 -0
  373. package/layers/ui/app/composables/useRecordOptions.ts +84 -0
  374. package/layers/ui/app/composables/useRepeater.ts +99 -0
  375. package/layers/ui/app/composables/useT.ts +32 -0
  376. package/layers/ui/app/composables/useToast.ts +46 -0
  377. package/layers/ui/app/i18n/de.ts +406 -0
  378. package/layers/ui/app/i18n/en.ts +440 -0
  379. package/layers/ui/app/utils/boolean-field.ts +10 -0
  380. package/layers/ui/app/utils/field-component.ts +22 -0
  381. package/layers/ui/app/utils/field-empty.ts +21 -0
  382. package/layers/ui/app/utils/field-registry.ts +36 -0
  383. package/layers/ui/app/utils/humanize.ts +14 -0
  384. package/layers/ui/app/utils/icons.ts +147 -0
  385. package/layers/ui/app/utils/iso-date.ts +18 -0
  386. package/layers/ui/app/utils/localized.ts +13 -0
  387. package/layers/ui/app/utils/record-context.ts +15 -0
  388. package/layers/ui/app/utils/reorder.ts +8 -0
  389. package/layers/ui/app/utils/richtext-preserve-class.ts +31 -0
  390. package/layers/ui/app/utils/useFieldA11y.ts +40 -0
  391. package/layers/ui/nuxt.config.ts +5 -0
  392. package/nuxt.config.ts +105 -0
  393. package/package.json +122 -0
  394. package/scripts/hash-password.mjs +26 -0
  395. package/scripts/typecheck.mjs +51 -0
@@ -0,0 +1,433 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue'
3
+ import type { BlockRow, BlockTreeCtx } from '../utils/block-tree'
4
+ import { resolveLocalized } from '../../../ui/app/utils/localized'
5
+ import BlockTree from './BlockTree.vue'
6
+
7
+ const props = withDefaults(
8
+ defineProps<{
9
+ blocks: BlockRow[]
10
+ selectedId: string | null
11
+ errorIds: Set<string>
12
+ /** Block ids that hold (or contain) a stale reference — a warning roll-up, parallel to `errorIds`. */
13
+ deadRefIds?: Set<string>
14
+ ctx: BlockTreeCtx
15
+ disabled?: boolean
16
+ /** Set on the parent block + slot when this list is a slot's children; null/null at the page root. */
17
+ parentId?: string | null
18
+ slotName?: string | null
19
+ /** Only the top-level instance renders the selectable "Page" root node. */
20
+ root?: boolean
21
+ }>(),
22
+ { deadRefIds: () => new Set<string>() },
23
+ )
24
+
25
+ const { t, lang } = useT()
26
+
27
+ const slotNamesOf = (type: string): string[] => props.ctx.byName[type]?.slots ?? []
28
+ const labelOf = (block: BlockRow): string => resolveLocalized(props.ctx.byName[block.type]?.label, lang.value) ?? block.type
29
+
30
+ // Inline "Add block" type picker. The block type is fixed at creation (not switchable afterwards), so
31
+ // the only type choice happens here. State is per tree instance, so the root list and each slot keep
32
+ // independent pickers. Inline (not teleported) keeps it directly DOM-testable.
33
+ const picking = ref(false)
34
+ function pick(type: string): void {
35
+ props.ctx.ops.add(props.parentId ?? null, props.slotName ?? null, type)
36
+ picking.value = false
37
+ }
38
+ </script>
39
+
40
+ <template>
41
+ <div class="block-tree">
42
+ <button
43
+ v-if="root"
44
+ type="button"
45
+ class="block-tree__root"
46
+ :class="{ 'block-tree__node-label--selected': selectedId === null }"
47
+ :aria-pressed="selectedId === null"
48
+ @click="ctx.ops.select(null)"
49
+ >
50
+ <UiIcon name="file-text" :size="15" class="block-tree__root-icon" />
51
+ <span>{{ t('blocks.page') }}</span>
52
+ </button>
53
+
54
+ <p v-if="root && !blocks.length" class="block-tree__empty">{{ t('blocks.empty') }}</p>
55
+
56
+ <ul class="block-tree__list">
57
+ <li
58
+ v-for="(block, i) in blocks"
59
+ :key="block.id"
60
+ class="block-tree__node"
61
+ :class="{ 'block-tree__node--selected': selectedId === block.id }"
62
+ :data-type="block.type"
63
+ >
64
+ <div class="block-tree__row">
65
+ <button
66
+ type="button"
67
+ class="block-tree__node-label"
68
+ :class="{ 'block-tree__node-label--selected': selectedId === block.id }"
69
+ :aria-pressed="selectedId === block.id"
70
+ @click="ctx.ops.select(block.id)"
71
+ >
72
+ <span class="block-tree__node-name">{{ labelOf(block) }}</span>
73
+ <span v-if="errorIds.has(block.id)" class="block-tree__badge" role="img" :aria-label="t('blocks.invalid')" :title="t('blocks.invalid')">!</span>
74
+ <UiIcon v-if="deadRefIds.has(block.id)" name="triangle-alert" :size="14" class="block-tree__deadbadge" role="img" :aria-label="t('deadRefs.blockBadge')" :title="t('deadRefs.blockBadge')" />
75
+ </button>
76
+ <div class="block-tree__actions">
77
+ <button type="button" class="block-tree__btn" :disabled="disabled || i === 0" :aria-label="t('blocks.moveUp', { n: i + 1 })" @click="ctx.ops.move(block.id, -1)"><UiIcon name="chevron-up" :size="15" /></button>
78
+ <button type="button" class="block-tree__btn" :disabled="disabled || i === blocks.length - 1" :aria-label="t('blocks.moveDown', { n: i + 1 })" @click="ctx.ops.move(block.id, 1)"><UiIcon name="chevron-down" :size="15" /></button>
79
+ <button type="button" class="block-tree__btn" :disabled="disabled" :aria-label="t('blocks.duplicate', { n: i + 1 })" @click="ctx.ops.duplicate(block.id)"><UiIcon name="copy" :size="15" /></button>
80
+ <button type="button" class="block-tree__btn block-tree__btn--danger" :disabled="disabled" :aria-label="t('blocks.remove', { n: i + 1 })" @click="ctx.ops.remove(block.id)"><UiIcon name="trash" :size="15" /></button>
81
+ </div>
82
+ </div>
83
+
84
+ <div v-if="slotNamesOf(block.type).length" class="block-tree__slots">
85
+ <div v-for="name in slotNamesOf(block.type)" :key="name" class="block-tree__slot">
86
+ <span class="block-tree__slot-label">{{ name }}</span>
87
+ <BlockTree
88
+ :blocks="(block.slots?.[name] as BlockRow[]) ?? []"
89
+ :selected-id="selectedId"
90
+ :error-ids="errorIds"
91
+ :dead-ref-ids="deadRefIds"
92
+ :ctx="ctx"
93
+ :disabled="disabled"
94
+ :parent-id="block.id"
95
+ :slot-name="name"
96
+ />
97
+ </div>
98
+ </div>
99
+ </li>
100
+ </ul>
101
+
102
+ <div class="block-tree__add">
103
+ <button
104
+ type="button"
105
+ class="block-tree__add-btn"
106
+ :class="{ 'block-tree__add-btn--active': picking }"
107
+ :disabled="disabled"
108
+ aria-haspopup="dialog"
109
+ :aria-expanded="picking"
110
+ @click="picking = true"
111
+ >
112
+ <UiIcon name="plus" :size="15" />
113
+ <span>{{ slotName ? t('blocks.addInto', { slot: slotName }) : t('blocks.add') }}</span>
114
+ </button>
115
+
116
+ <!-- The type picker lives in a centered modal (position:fixed) so it escapes the tree pane's
117
+ overflow-y:auto clip; reka's DialogRoot owns Escape/outside-click dismissal + focus restore. -->
118
+ <UiDialog :open="picking" size="lg" :title="t('blocks.pickType')" @update:open="picking = $event">
119
+ <!-- future: category tabs + a search input mount here, above the grid -->
120
+ <ul class="block-tree__picker" :aria-label="t('blocks.pickType')">
121
+ <li v-for="bt in ctx.allowedTypes" :key="bt.name">
122
+ <button
123
+ type="button"
124
+ class="block-tree__picker-item"
125
+ :disabled="disabled"
126
+ :aria-label="t('blocks.addOfType', { type: resolveLocalized(bt.label, lang) ?? bt.name })"
127
+ @click="pick(bt.name)"
128
+ >
129
+ <img v-if="bt.image" :src="bt.image" alt="" loading="lazy" class="block-tree__picker-img" />
130
+ <UiIcon v-else :name="bt.icon ?? 'layout-grid'" :size="24" class="block-tree__picker-icon" />
131
+ <span class="block-tree__picker-label">{{ resolveLocalized(bt.label, lang) ?? bt.name }}</span>
132
+ </button>
133
+ </li>
134
+ </ul>
135
+ </UiDialog>
136
+ </div>
137
+ </div>
138
+ </template>
139
+
140
+ <style lang="scss">
141
+ .block-tree {
142
+ display: flex;
143
+ flex-direction: column;
144
+ gap: var(--space-1);
145
+ font-size: var(--text-sm);
146
+
147
+ &__root,
148
+ &__node-label {
149
+ display: flex;
150
+ align-items: center;
151
+ gap: var(--space-2);
152
+ flex: 1;
153
+ min-width: 0;
154
+ padding: var(--space-1) var(--space-2);
155
+ border: 0;
156
+ border-radius: var(--radius-sm);
157
+ background: transparent;
158
+ font: inherit;
159
+ text-align: left;
160
+ text-transform: capitalize;
161
+ color: var(--color-text);
162
+ cursor: pointer;
163
+
164
+ &:hover {
165
+ background: var(--color-hover);
166
+ }
167
+ &:focus-visible {
168
+ outline: 2px solid var(--color-focus);
169
+ outline-offset: -2px;
170
+ }
171
+ }
172
+
173
+ // The page root reads as a quieter, structural header than the blocks below it.
174
+ &__root {
175
+ font-weight: var(--weight-medium);
176
+ color: var(--color-text-muted);
177
+ }
178
+ &__root-icon {
179
+ flex-shrink: 0;
180
+ color: var(--color-text-subtle);
181
+ }
182
+
183
+ &__node-label--selected {
184
+ background: var(--color-active, var(--color-surface-2));
185
+ color: var(--color-text);
186
+ font-weight: var(--weight-medium);
187
+ box-shadow: inset 2px 0 0 var(--color-primary);
188
+ }
189
+
190
+ &__node-name {
191
+ overflow: hidden;
192
+ text-overflow: ellipsis;
193
+ white-space: nowrap;
194
+ }
195
+
196
+ &__empty {
197
+ margin: 0;
198
+ padding: var(--space-1) var(--space-2);
199
+ color: var(--color-text-muted);
200
+ }
201
+
202
+ &__list {
203
+ list-style: none;
204
+ margin: 0;
205
+ padding: 0;
206
+ display: flex;
207
+ flex-direction: column;
208
+ gap: 2px;
209
+ }
210
+ &__node {
211
+ display: flex;
212
+ flex-direction: column;
213
+ gap: var(--space-1);
214
+ }
215
+ &__row {
216
+ position: relative;
217
+ display: flex;
218
+ align-items: center;
219
+ gap: var(--space-1);
220
+ border-radius: var(--radius-sm);
221
+ }
222
+
223
+ &__badge {
224
+ display: inline-flex;
225
+ align-items: center;
226
+ justify-content: center;
227
+ flex-shrink: 0;
228
+ width: 1rem;
229
+ height: 1rem;
230
+ border-radius: var(--radius-full);
231
+ background: var(--color-danger-solid);
232
+ color: var(--color-on-danger);
233
+ font-size: var(--text-xs);
234
+ font-weight: var(--weight-bold);
235
+ line-height: 1;
236
+ }
237
+
238
+ // Stale-reference warning marker (distinct shape + colour from the red error badge).
239
+ &__deadbadge {
240
+ flex-shrink: 0;
241
+ color: var(--color-warning);
242
+ }
243
+
244
+ // Structural controls stay quiet until the row is hovered, focused, or selected — the tree reads as
245
+ // a calm outline, not a control panel. Overlaid (position:absolute) so the hidden bar reserves NO
246
+ // width and the label spans the full row when idle. Kept in the DOM (revealed by :focus-within) so
247
+ // they stay keyboard-reachable; the pointer-events toggle lets idle clicks fall through to the label.
248
+ &__actions {
249
+ position: absolute;
250
+ top: 0;
251
+ right: 0;
252
+ bottom: 0;
253
+ display: flex;
254
+ align-items: center;
255
+ gap: 1px;
256
+ padding-left: var(--space-5);
257
+ border-radius: var(--radius-sm);
258
+ background: linear-gradient(to right, transparent, var(--color-surface) var(--space-4));
259
+ opacity: 0;
260
+ pointer-events: none;
261
+ transition: opacity var(--motion-fast) var(--ease-standard);
262
+ }
263
+ &__row:hover &__actions,
264
+ &__row:focus-within &__actions,
265
+ &__node--selected > &__row &__actions {
266
+ opacity: 1;
267
+ pointer-events: auto;
268
+ }
269
+ // When the overlay is revealed, reserve room on the label so the name and the validity / stale-ref
270
+ // badges aren't hidden under it (4×1.5rem buttons + a little clearance). Idle rows keep the full width.
271
+ &__row:hover &__node-label,
272
+ &__row:focus-within &__node-label,
273
+ &__node--selected > &__row &__node-label {
274
+ padding-right: 6.25rem;
275
+ }
276
+
277
+ &__btn {
278
+ display: flex;
279
+ align-items: center;
280
+ justify-content: center;
281
+ width: 1.5rem;
282
+ height: 1.5rem;
283
+ border: 0;
284
+ border-radius: var(--radius-sm);
285
+ background: transparent;
286
+ color: var(--color-text-muted); /* meaningful icon control at rest → ≥3:1 (SC 1.4.11) */
287
+ cursor: pointer;
288
+ transition:
289
+ background-color var(--motion-fast) var(--ease-standard),
290
+ color var(--motion-fast) var(--ease-standard);
291
+
292
+ &:hover:not(:disabled) {
293
+ background: var(--color-hover);
294
+ color: var(--color-text);
295
+ }
296
+ &:focus-visible {
297
+ outline: 2px solid var(--color-focus);
298
+ outline-offset: -2px;
299
+ }
300
+ &:disabled {
301
+ opacity: 0.35;
302
+ cursor: default;
303
+ }
304
+ &--danger:hover:not(:disabled) {
305
+ color: var(--color-danger);
306
+ }
307
+ }
308
+
309
+ &__slots {
310
+ display: flex;
311
+ flex-direction: column;
312
+ gap: var(--space-1);
313
+ // Subtle step (~13px/level) so deep nests don't eat the label width; the 1px guide keeps it legible.
314
+ margin: 2px 0 var(--space-1) var(--space-1);
315
+ padding-left: var(--space-2);
316
+ border-left: 1px solid var(--color-border);
317
+ }
318
+ &__slot {
319
+ display: flex;
320
+ flex-direction: column;
321
+ gap: 2px;
322
+ }
323
+ &__slot-label {
324
+ padding: 0 var(--space-2);
325
+ font-size: var(--text-xs);
326
+ font-weight: var(--weight-medium);
327
+ color: var(--color-text-muted);
328
+ text-transform: capitalize;
329
+ }
330
+
331
+ &__add {
332
+ position: relative;
333
+ margin-top: 2px;
334
+ }
335
+ &__add-btn {
336
+ display: flex;
337
+ align-items: center;
338
+ gap: var(--space-2);
339
+ width: 100%;
340
+ padding: var(--space-1) var(--space-2);
341
+ border: 0;
342
+ border-radius: var(--radius-sm);
343
+ background: transparent;
344
+ color: var(--color-text-muted);
345
+ font: inherit;
346
+ font-size: var(--text-sm);
347
+ text-align: left;
348
+ cursor: pointer;
349
+ transition:
350
+ background-color var(--motion-fast) var(--ease-standard),
351
+ color var(--motion-fast) var(--ease-standard);
352
+
353
+ &:hover:not(:disabled) {
354
+ background: var(--color-hover);
355
+ color: var(--color-text);
356
+ }
357
+ &:focus-visible {
358
+ outline: 2px solid var(--color-focus);
359
+ outline-offset: -2px;
360
+ }
361
+ &:disabled {
362
+ opacity: 0.5;
363
+ cursor: default;
364
+ }
365
+ }
366
+ // While its picker modal is open, the trigger carries a high-contrast solid-indigo fill so it reads as
367
+ // the active insertion target (UiIcon uses currentColor, so the + glyph flips to on-primary too).
368
+ &__add-btn--active,
369
+ &__add-btn--active:hover:not(:disabled) {
370
+ background: var(--color-primary-solid);
371
+ color: var(--color-on-primary);
372
+ }
373
+
374
+ // Tile grid inside the picker dialog body (the dialog provides the card chrome). Structured so a future
375
+ // category-tab row + search input can mount above it. Tiles widen to hold optional preview screenshots.
376
+ &__picker {
377
+ list-style: none;
378
+ margin: 0;
379
+ padding: 0;
380
+ display: grid;
381
+ grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr));
382
+ gap: var(--space-2);
383
+ }
384
+ &__picker-item {
385
+ display: flex;
386
+ flex-direction: column;
387
+ align-items: center;
388
+ justify-content: center;
389
+ gap: var(--space-2);
390
+ width: 100%;
391
+ padding: var(--space-2);
392
+ border: 1px solid transparent;
393
+ border-radius: var(--radius-sm);
394
+ background: transparent;
395
+ color: var(--color-text);
396
+ font: inherit;
397
+ font-size: var(--text-xs);
398
+ line-height: 1.2;
399
+ text-align: center;
400
+ text-transform: capitalize;
401
+ cursor: pointer;
402
+
403
+ &:hover:not(:disabled) {
404
+ background: var(--color-hover);
405
+ border-color: var(--color-border);
406
+ }
407
+ &:focus-visible {
408
+ outline: 2px solid var(--color-focus);
409
+ outline-offset: -2px;
410
+ }
411
+ &:disabled {
412
+ opacity: 0.5;
413
+ cursor: default;
414
+ }
415
+ }
416
+ &__picker-icon {
417
+ color: var(--color-text-subtle);
418
+ }
419
+ &__picker-img {
420
+ width: 100%;
421
+ aspect-ratio: 16 / 10;
422
+ object-fit: cover;
423
+ border: 1px solid var(--color-border);
424
+ border-radius: var(--radius-sm);
425
+ }
426
+ &__picker-label {
427
+ display: block;
428
+ max-width: 100%;
429
+ overflow: hidden;
430
+ text-overflow: ellipsis;
431
+ }
432
+ }
433
+ </style>
@@ -0,0 +1,162 @@
1
+ <script setup lang="ts">
2
+ import type { SerializedBlock } from '../../../core/server/utils/serialize-collection'
3
+ import type { BlockTreeCtx } from '../utils/block-tree'
4
+
5
+ // The page-builder editor body: hierarchy · live preview · contextual fields. All form state comes from
6
+ // the editor form context (CollectionEditor); the block-specific machinery lives here.
7
+ const { t } = useT()
8
+ const ctx = useEditorFormContext()
9
+ const { values, setField, blockErrors, deadRefs, saving, locale, blocksAllowed } = ctx
10
+
11
+ // Block registry. This body mounts only for a blocks-enabled collection, so the load always runs.
12
+ const { load } = useBlocks()
13
+ const all = await load()
14
+ const byName = computed<Record<string, SerializedBlock>>(() => Object.fromEntries(all.map((b) => [b.name, b])))
15
+ const allowedTypes = computed(() => (blocksAllowed.value?.length ? all.filter((b) => blocksAllowed.value!.includes(b.name)) : all))
16
+
17
+ // One writable view of `content` so every tree mutation routes back through `setField` (→ block-error
18
+ // reconciliation).
19
+ const content = computed<unknown[]>({
20
+ get: () => (values.content as unknown[]) ?? [],
21
+ set: (v) => setField('content', v),
22
+ })
23
+ const tree = useBlockTree(content, byName, undefined, (v, coalesceAs) => setField('content', v, coalesceAs))
24
+ const { selectedId, selectedBlock } = tree
25
+
26
+ // Tree badges: a node is flagged if it (or any descendant, by roll-up) has an error.
27
+ const errorIds = computed(() => errorBearingIds(tree.blocks.value, new Set(Object.keys(blockErrors.value))))
28
+ // Stale-reference warnings (derived on read): the same ancestor roll-up as errors, plus the per-field set
29
+ // for the selected block. (The page-root dead fields live on the shell's shared page-fields bindings.)
30
+ const deadRefIds = computed(() => errorBearingIds(tree.blocks.value, deadBlockIds(deadRefs.value)))
31
+ const blockDeadFields = computed(() => deadFieldsAt(deadRefs.value, selectedId.value))
32
+
33
+ // Stable config + ops threaded through the recursive tree.
34
+ const treeCtx: BlockTreeCtx = {
35
+ byName: byName.value,
36
+ allowedTypes: allowedTypes.value,
37
+ ops: { select: tree.select, add: tree.add, remove: tree.remove, move: tree.move, duplicate: tree.duplicate },
38
+ }
39
+
40
+ // On a failed save: jump to the first offending block so its fields are in view; if none, deselect so the
41
+ // page-fields pane remounts and its inline error shows. Registered with the shell, which invokes it.
42
+ ctx.registerRevealError(() => {
43
+ const firstBlock = Object.keys(blockErrors.value)[0]
44
+ if (firstBlock) tree.select(firstBlock)
45
+ else if (selectedId.value) tree.select(null)
46
+ })
47
+
48
+ // Cmd/Ctrl+Z undo, Cmd/Ctrl+Shift+Z (or Ctrl+Y) redo — for the block editor. Skipped while editing text
49
+ // (input/textarea/contenteditable) so the field's / richtext's own native undo keeps working.
50
+ function onKeydown(e: KeyboardEvent) {
51
+ if (!(e.metaKey || e.ctrlKey)) return
52
+ const k = e.key.toLowerCase()
53
+ const isRedo = k === 'y' || (k === 'z' && e.shiftKey)
54
+ const isUndo = k === 'z' && !e.shiftKey
55
+ if (!isUndo && !isRedo) return
56
+ const el = e.target as HTMLElement | null
57
+ if (el && (el.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName))) return
58
+ e.preventDefault()
59
+ isRedo ? ctx.redo() : ctx.undo()
60
+ }
61
+ onMounted(() => window.addEventListener('keydown', onKeydown))
62
+ onUnmounted(() => window.removeEventListener('keydown', onKeydown))
63
+ </script>
64
+
65
+ <template>
66
+ <div class="editor3">
67
+ <nav class="editor3__tree" :aria-label="t('blocks.treeLabel')">
68
+ <div class="editor3__tree-head">
69
+ <p class="editor3__pane-label">{{ t('blocks.treeLabel') }}</p>
70
+ </div>
71
+ <BlockTree
72
+ root
73
+ :blocks="tree.blocks.value"
74
+ :selected-id="selectedId"
75
+ :error-ids="errorIds"
76
+ :dead-ref-ids="deadRefIds"
77
+ :ctx="treeCtx"
78
+ :disabled="saving"
79
+ />
80
+ </nav>
81
+
82
+ <aside class="editor3__preview" :aria-label="t('preview.ariaLabel')">
83
+ <!-- The "Preview" pane label is folded into BlockPreview's own toolbar row (single-line header). -->
84
+ <BlockPreview :content="content" :locale="locale" :selected-id="selectedId" @select="tree.select" />
85
+ </aside>
86
+
87
+ <section class="editor3__fields" :aria-label="t('blocks.fieldsLabel')">
88
+ <!-- A block is selected → its fields; otherwise the page (collection) fields + locale switcher. -->
89
+ <BlockFields
90
+ v-if="selectedBlock"
91
+ :block="selectedBlock"
92
+ :def="byName[selectedBlock.type]"
93
+ :locale="locale"
94
+ :errors="selectedId ? blockErrors[selectedId] : undefined"
95
+ :dead-fields="blockDeadFields"
96
+ :disabled="saving"
97
+ @update="(k, v) => selectedId && tree.setProp(selectedId, k, v)"
98
+ />
99
+ <template v-else>
100
+ <p class="editor3__pane-label">{{ t('blocks.pageFields') }}</p>
101
+ <PageFieldsPane v-bind="ctx.pageFieldsBindings.value" v-on="ctx.pageFieldsHandlers" />
102
+ </template>
103
+ </section>
104
+ </div>
105
+ </template>
106
+
107
+ <style lang="scss">
108
+ .editor3 {
109
+ display: grid;
110
+ grid-template-columns: minmax(0, 17rem) minmax(0, 1fr) minmax(0, 22rem);
111
+ gap: var(--space-4);
112
+ flex: 1 1 auto;
113
+ min-height: 0;
114
+
115
+ @media (max-width: 1100px) {
116
+ grid-template-columns: 1fr;
117
+ // Stacked single column: the grid scrolls as a whole; panes grow to their content.
118
+ overflow-y: auto;
119
+ .editor3__tree,
120
+ .editor3__preview,
121
+ .editor3__fields {
122
+ overflow: visible;
123
+ }
124
+ }
125
+
126
+ &__pane-label {
127
+ margin: 0;
128
+ font-size: var(--text-sm);
129
+ font-weight: var(--weight-medium);
130
+ color: var(--color-text-muted);
131
+ }
132
+
133
+ &__tree-head {
134
+ display: flex;
135
+ align-items: center;
136
+ justify-content: space-between;
137
+ gap: var(--space-2);
138
+ }
139
+
140
+ // Each pane fills the column height and scrolls independently. Tree + fields are the side panels;
141
+ // the preview in the middle is the canvas they act on.
142
+ &__tree,
143
+ &__fields,
144
+ &__preview {
145
+ min-height: 0;
146
+ overflow-y: auto;
147
+ display: flex;
148
+ flex-direction: column;
149
+ gap: var(--space-3);
150
+ padding: var(--space-4);
151
+ border: 1px solid var(--color-border);
152
+ border-radius: var(--radius-lg);
153
+ background: var(--color-surface);
154
+ }
155
+ &__fields {
156
+ gap: var(--space-4);
157
+ }
158
+ &__preview {
159
+ background: var(--color-bg);
160
+ }
161
+ }
162
+ </style>
@@ -0,0 +1,38 @@
1
+ <script setup lang="ts">
2
+ import type { BatchDeleteReport } from '../utils/collection-ops'
3
+
4
+ // The destructive-action confirm (replaces window.confirm). Shows how many rows will be deleted and, when
5
+ // any are still referenced by other records, the aggregated "left with a broken link" warning fetched from
6
+ // the referrer endpoint. Mirrors media/MediaDeleteDialog one layer over.
7
+ defineProps<{ open: boolean; report: BatchDeleteReport | null; busy?: boolean; error?: string | null }>()
8
+ const emit = defineEmits<{ confirm: []; 'update:open': [boolean] }>()
9
+
10
+ const { t } = useT()
11
+ </script>
12
+
13
+ <template>
14
+ <UiDialog :open="open" :title="t('common.delete')" @update:open="(v) => emit('update:open', v)">
15
+ <p v-if="report">{{ t('list.deleteSummary', { n: report.count }) }}</p>
16
+ <div v-if="report && report.referencedCount > 0" class="collection-delete__warn">
17
+ <p>{{ t('list.deleteReferrersWarn', { n: report.referencedCount }) }}</p>
18
+ </div>
19
+ <!-- The referrer lookup FAILED (not "none found"): inbound links are unknown, so caution rather than
20
+ imply a safe delete. Distinct from the reference warning above, which needs a successful check. -->
21
+ <UiAlert v-if="report && report.checked === false" variant="warning" class="collection-delete__caution">
22
+ {{ t('list.deleteRefsUnverified') }}
23
+ </UiAlert>
24
+ <UiAlert v-if="error" variant="error">{{ error }}</UiAlert>
25
+ <template #footer>
26
+ <UiButton variant="ghost" :disabled="busy" @click="emit('update:open', false)">{{ t('common.cancel') }}</UiButton>
27
+ <UiButton variant="danger" :disabled="busy" @click="emit('confirm')">{{ t('common.delete') }}</UiButton>
28
+ </template>
29
+ </UiDialog>
30
+ </template>
31
+
32
+ <style lang="scss" scoped>
33
+ .collection-delete__warn {
34
+ margin-top: var(--space-3);
35
+ color: var(--color-danger);
36
+ font-size: var(--text-sm);
37
+ }
38
+ </style>