@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,71 @@
1
+ import type { Condition, ConditionOperator, ConditionRule } from '../../../core/server/utils/defineCollection'
2
+
3
+ /** Empty = absent or a "no value" (null/undefined/''/[]). Shared by the evaluator (the `empty`
4
+ * operator + bare-rule presence) and the server's required-when-visible check. 0 and false are values. */
5
+ export function isEmptyValue(value: unknown): boolean {
6
+ return value === null || value === undefined || value === '' || (Array.isArray(value) && value.length === 0)
7
+ }
8
+
9
+ // v1 path scope is SIBLING-ONLY. A bare name or `./name` resolves a sibling in the current scope;
10
+ // cross-scope forms (`/abs`, `../parent`, dotted `a.b`) are out of v1 and resolve to undefined — so the
11
+ // rule is simply false (the field hides), keeping the evaluator total: it never throws on a bad path.
12
+ function resolvePath(field: string, scope: Record<string, unknown>): unknown {
13
+ const key = field.startsWith('./') ? field.slice(2) : field
14
+ if (key === '' || key.startsWith('/') || key.startsWith('..') || key.includes('/') || key.includes('.')) {
15
+ return undefined
16
+ }
17
+ return scope[key]
18
+ }
19
+
20
+ function compare(dep: unknown, target: number | string, op: 'gt' | 'gte' | 'lt' | 'lte'): boolean {
21
+ if (typeof dep !== typeof target || (typeof dep !== 'number' && typeof dep !== 'string')) return false
22
+ if (op === 'gt') return dep > target
23
+ if (op === 'gte') return dep >= target
24
+ if (op === 'lt') return dep < target
25
+ return dep <= target
26
+ }
27
+
28
+ function matchOperator(dep: unknown, op: ConditionOperator): boolean {
29
+ for (const [key, target] of Object.entries(op)) {
30
+ if (target === undefined) continue
31
+ let ok: boolean
32
+ switch (key) {
33
+ case 'eq': ok = dep === target; break
34
+ case 'ne': ok = dep !== target; break
35
+ case 'gt': case 'gte': case 'lt': case 'lte':
36
+ ok = compare(dep, target as number | string, key); break
37
+ case 'in': ok = Array.isArray(target) && (target as unknown[]).includes(dep); break
38
+ case 'notIn': ok = Array.isArray(target) && !(target as unknown[]).includes(dep); break
39
+ case 'regexp': {
40
+ if (typeof dep !== 'string' || typeof target !== 'string') { ok = false; break }
41
+ try { ok = new RegExp(target).test(dep) } catch { ok = false }
42
+ break
43
+ }
44
+ case 'empty': ok = target ? isEmptyValue(dep) : !isEmptyValue(dep); break
45
+ default: ok = false
46
+ }
47
+ if (!ok) return false
48
+ }
49
+ return true
50
+ }
51
+
52
+ function evaluateRule(rule: ConditionRule, scope: Record<string, unknown>): boolean {
53
+ const dep = resolvePath(rule.field, scope)
54
+ if (rule.is !== undefined) return dep === rule.is
55
+ if (rule.op) return matchOperator(dep, rule.op)
56
+ return !isEmptyValue(dep)
57
+ }
58
+
59
+ /** Pure, total (never throws) condition evaluator shared by the editor (hide widgets) and the server
60
+ * (required-when-visible). `scope` is the sibling-value map the condition resolves against. */
61
+ export function evaluateCondition(condition: Condition, scope: Record<string, unknown>): boolean {
62
+ if ('and' in condition) return condition.and.every((c) => evaluateCondition(c, scope))
63
+ if ('or' in condition) return condition.or.some((c) => evaluateCondition(c, scope))
64
+ if ('not' in condition) return !evaluateCondition(condition.not, scope)
65
+ return evaluateRule(condition, scope)
66
+ }
67
+
68
+ /** Whether a field is shown given the sibling-value `scope`. No condition => always visible. */
69
+ export function isFieldVisible(field: { condition?: Condition } | null | undefined, scope: Record<string, unknown>): boolean {
70
+ return !field?.condition || evaluateCondition(field.condition, scope)
71
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Runtime no-op for a block SFC's metadata call — `defineBlock({ label, slots, icon })`. The metadata is
3
+ * read at BUILD time by the SFC extractor (from the same call, statically); at runtime this only exists so
4
+ * the SFC's `<script setup>` doesn't reference an undefined `defineBlock`. Auto-imported in the app so block
5
+ * authors call it without an import (the field factories are auto-imported the same way).
6
+ */
7
+ export function defineBlock(_meta?: {
8
+ label?: string | Record<string, string>
9
+ slots?: string[]
10
+ icon?: string
11
+ }): void {
12
+ // intentionally empty — see above.
13
+ }
@@ -0,0 +1,17 @@
1
+ import type { FieldDef } from '../../../core/server/utils/defineCollection'
2
+
3
+ type NumberOptions = Extract<FieldDef, { type: 'number' }>['options']
4
+ type ChoiceOptions = Extract<FieldDef, { type: 'choice' }>['options']
5
+
6
+ /**
7
+ * Whether a number field is integer-valued — the default — i.e. neither
8
+ * `integer: false` nor a `decimals` precision opts it into reals.
9
+ */
10
+ export function numberIsInteger(options: NumberOptions): boolean {
11
+ return options?.integer !== false && options?.decimals === undefined
12
+ }
13
+
14
+ /** The allowed values of a choice field, as a non-empty tuple (for `z.enum`). */
15
+ export function choiceValues(options: ChoiceOptions): [string, ...string[]] {
16
+ return options.choices.map((c) => c.value) as [string, ...string[]]
17
+ }
@@ -0,0 +1,129 @@
1
+ import type { FieldDef } from '../../../core/server/utils/defineCollection'
2
+
3
+ /**
4
+ * Field factories for single-file (SFC) block authoring, à la Pruvious. A block's schema is declared as its
5
+ * component's `defineProps({ heading: textField({ required: true }), … })`; each factory returns a DUAL
6
+ * object — a Vue prop descriptor (`type` = a JS-global constructor) that ALSO carries the Kestrel `FieldDef`
7
+ * under `[KESTREL_FIELD]`. Vue reads `type` at runtime; the build-time SFC extractor reads `[KESTREL_FIELD]`.
8
+ *
9
+ * This file imports NO Vue/Nuxt runtime (only JS globals), so it is importable BOTH in the browser (the SFC's
10
+ * props declaration) AND by the Node extractor that injects these factories to evaluate a block's schema.
11
+ * `Symbol.for` (global registry) keeps the key identical even if the file is instantiated twice.
12
+ */
13
+ export const KESTREL_FIELD = Symbol.for('kestrel.field')
14
+
15
+ export interface FieldFactoryResult<C = unknown> {
16
+ /** A Vue prop constructor (String / Number / Boolean / Object / Array) — the runtime prop shape. Typed as
17
+ * the specific constructor so Vue's `defineProps` accepts the factory result AND infers the prop's value
18
+ * type (`textField()` → `string`, `mediaField()` → `number`, …). */
19
+ type: C
20
+ /** The Kestrel field definition the extractor lifts into the block's schema. */
21
+ [KESTREL_FIELD]: FieldDef
22
+ }
23
+
24
+ // Keys that belong on the FieldDef itself (not inside `options`); everything else a factory receives is a
25
+ // type-specific option. Mirrors `BaseFieldDef` in defineCollection.ts.
26
+ const BASE_KEYS = new Set(['required', 'translatable', 'unique', 'label', 'default', 'condition', 'renamedFrom', 'populate'])
27
+
28
+ function partition(opts: Record<string, unknown>): { base: Record<string, unknown>; options: Record<string, unknown> } {
29
+ const base: Record<string, unknown> = {}
30
+ const options: Record<string, unknown> = {}
31
+ for (const [k, v] of Object.entries(opts)) (BASE_KEYS.has(k) ? base : options)[k] = v
32
+ return { base, options }
33
+ }
34
+
35
+ function wrap<C>(def: Record<string, unknown>, vueType: C): FieldFactoryResult<C> {
36
+ // The factory assembles the FieldDef structurally (open discriminated union); cast through `unknown`.
37
+ return { type: vueType, [KESTREL_FIELD]: def as unknown as FieldDef }
38
+ }
39
+
40
+ /** A leaf field: base props at the top level, everything else under `options`. */
41
+ function leaf<C>(type: string, vueType: C) {
42
+ return (opts: Record<string, unknown> = {}): FieldFactoryResult<C> => {
43
+ const { base, options } = partition(opts)
44
+ const def: Record<string, unknown> = { type, ...base }
45
+ if (Object.keys(options).length) def.options = options
46
+ return wrap(def, vueType)
47
+ }
48
+ }
49
+
50
+ /** Generic escape hatch for a CUSTOM field type (registered via `defineFieldType`) that has no dedicated
51
+ * factory — `field('secureGallery', { … })`. Base props split off the top level, the rest into `options`;
52
+ * the Vue prop is typed `Object` (unknown value shape). */
53
+ export function field(type: string, opts: Record<string, unknown> = {}): FieldFactoryResult<ObjectConstructor> {
54
+ const { base, options } = partition(opts)
55
+ const def: Record<string, unknown> = { type, ...base }
56
+ if (Object.keys(options).length) def.options = options
57
+ return wrap(def, Object)
58
+ }
59
+
60
+ export const textField = leaf('text', String)
61
+ export const slugField = leaf('slug', String)
62
+ export const richtextField = leaf('richtext', String)
63
+ export const numberField = leaf('number', Number)
64
+ export const booleanField = leaf('boolean', Boolean)
65
+ export const jsonField = leaf('json', Object)
66
+ export const linkField = leaf('link', Object)
67
+
68
+ /** Media: `multiple` (stored bare, an id array) → Array prop; single (a `<name>Id` FK) → Number prop. */
69
+ export function mediaField(opts: Record<string, unknown> = {}): FieldFactoryResult<ArrayConstructor | NumberConstructor> {
70
+ const { base, options } = partition(opts)
71
+ const def: Record<string, unknown> = { type: 'media', ...base }
72
+ if (Object.keys(options).length) def.options = options
73
+ return wrap(def, options.multiple ? Array : Number)
74
+ }
75
+
76
+ /** Choice: `multiple` → the value is a `string[]` (Array prop), single → a `string` (String prop). */
77
+ export function choiceField(opts: Record<string, unknown> = {}): FieldFactoryResult<ArrayConstructor | StringConstructor> {
78
+ const { base, options } = partition(opts)
79
+ const def: Record<string, unknown> = { type: 'choice', ...base }
80
+ if (Object.keys(options).length) def.options = options
81
+ return wrap(def, options.multiple ? Array : String)
82
+ }
83
+
84
+ /** Datetime: `range` → the value is a `{ start, end }` object (Object prop), else a `string` (String prop). */
85
+ export function datetimeField(opts: Record<string, unknown> = {}): FieldFactoryResult<ObjectConstructor | StringConstructor> {
86
+ const { base, options } = partition(opts)
87
+ const def: Record<string, unknown> = { type: 'datetime', ...base }
88
+ if (Object.keys(options).length) def.options = options
89
+ return wrap(def, options.range ? Object : String)
90
+ }
91
+
92
+ /** Relation: `collection`/`many`/`labelField` map into `relation`; `many` → Array prop, single → Number. */
93
+ export function relationField(opts: Record<string, unknown> = {}): FieldFactoryResult<ArrayConstructor | NumberConstructor> {
94
+ const { collection, many, labelField, ...rest } = opts as {
95
+ collection?: string
96
+ many?: boolean
97
+ labelField?: string
98
+ } & Record<string, unknown>
99
+ if (typeof collection !== 'string' || !collection) {
100
+ throw new Error('relationField requires a `collection` (the target collection name)')
101
+ }
102
+ const { base, options } = partition(rest)
103
+ const relation: Record<string, unknown> = { collection }
104
+ if (many) relation.many = many
105
+ if (labelField) relation.labelField = labelField
106
+ const def: Record<string, unknown> = { type: 'relation', ...base, relation }
107
+ if (Object.keys(options).length) def.options = options
108
+ return wrap(def, many ? Array : Number)
109
+ }
110
+
111
+ /** Repeater: sub-fields are authored as factory calls too — unwrap each to its FieldDef (recursively).
112
+ * `fieldLayout` (the sub-field editor layout) forwards into `options` instead of being dropped. */
113
+ export function repeaterField(opts: Record<string, unknown> = {}): FieldFactoryResult<ArrayConstructor> {
114
+ const { fields, fieldLayout, ...rest } = opts as { fields?: Record<string, unknown>; fieldLayout?: unknown } & Record<string, unknown>
115
+ const { base } = partition(rest)
116
+ const options: Record<string, unknown> = { fields: unwrapFields(fields ?? {}) }
117
+ if (fieldLayout !== undefined) options.fieldLayout = fieldLayout
118
+ return wrap({ type: 'repeater', ...base, options }, Array)
119
+ }
120
+
121
+ /** Convert a `{ name: <factory-result | FieldDef> }` map into a `{ name: FieldDef }` map. A nested repeater's
122
+ * result already carries a fully-unwrapped def, so recursion is implicit. */
123
+ function unwrapFields(fields: Record<string, unknown>): Record<string, FieldDef> {
124
+ const out: Record<string, FieldDef> = {}
125
+ for (const [k, v] of Object.entries(fields)) {
126
+ out[k] = v && typeof v === 'object' && KESTREL_FIELD in v ? (v as FieldFactoryResult)[KESTREL_FIELD] : (v as FieldDef)
127
+ }
128
+ return out
129
+ }
@@ -0,0 +1,33 @@
1
+ import type { FieldDef, FieldOf } from '../../../core/server/utils/defineCollection'
2
+ import { numberIsInteger } from './field-constraints'
3
+
4
+ export interface FieldConstraints {
5
+ required: boolean
6
+ minlength?: number
7
+ maxlength?: number
8
+ min?: number
9
+ max?: number
10
+ step?: number | 'any'
11
+ multiline?: boolean
12
+ }
13
+
14
+ /** Input-relevant attributes derived from a FieldDef, for the field widgets. */
15
+ export function fieldConstraints(field: FieldDef): FieldConstraints {
16
+ const c: FieldConstraints = { required: !!field.required }
17
+ // `field as FieldOf<'x'>`: the open consumer arm makes `type` a non-discriminant, so `field.type === 'x'`
18
+ // doesn't narrow `field` — the cast applies the arm the guard just proved. (App-side: type-only, no
19
+ // runtime import of the server-side `fieldIs` guard into the client bundle.)
20
+ if (field.type === 'text') {
21
+ const f = field as FieldOf<'text'>
22
+ if (f.options?.minLength !== undefined) c.minlength = f.options.minLength
23
+ if (f.options?.maxLength !== undefined) c.maxlength = f.options.maxLength
24
+ if (f.options?.multiline) c.multiline = true
25
+ } else if (field.type === 'number') {
26
+ const f = field as FieldOf<'number'>
27
+ if (f.options?.min !== undefined) c.min = f.options.min
28
+ if (f.options?.max !== undefined) c.max = f.options.max
29
+ if (f.options?.decimals !== undefined) c.step = 1 / 10 ** f.options.decimals
30
+ else c.step = numberIsInteger(f.options) ? 1 : 'any'
31
+ }
32
+ return c
33
+ }
@@ -0,0 +1,75 @@
1
+ import type { FieldDef, FieldOf } from '../../../core/server/utils/defineCollection'
2
+ import { choiceValues, numberIsInteger } from './field-constraints'
3
+
4
+ function isEmpty(value: unknown): boolean {
5
+ return value === undefined || value === null || value === ''
6
+ }
7
+
8
+ /**
9
+ * Advisory client-side validation for instant feedback. A lightweight mirror of a
10
+ * subset of the server rules — the server Zod schema stays the sole authority.
11
+ */
12
+ export function validateField(field: FieldDef, value: unknown): string | null {
13
+ if (isEmpty(value)) return field.required ? 'This field is required.' : null
14
+
15
+ switch (field.type) {
16
+ case 'text': {
17
+ if (typeof value !== 'string') return null
18
+ const { minLength, maxLength } = (field as FieldOf<'text'>).options ?? {}
19
+ if (minLength !== undefined && value.length < minLength) return `Must be at least ${minLength} characters.`
20
+ if (maxLength !== undefined && value.length > maxLength) return `Must be at most ${maxLength} characters.`
21
+ return null
22
+ }
23
+ case 'number': {
24
+ if (typeof value !== 'number' || Number.isNaN(value)) return 'Must be a number.'
25
+ const opts = (field as FieldOf<'number'>).options
26
+ const { min, max } = opts ?? {}
27
+ if (numberIsInteger(opts) && !Number.isInteger(value)) return 'Must be a whole number.'
28
+ if (min !== undefined && value < min) return `Must be at least ${min}.`
29
+ if (max !== undefined && value > max) return `Must be at most ${max}.`
30
+ return null
31
+ }
32
+ // The widget model is always already-parsed (Json.vue stores `JSON.parse(raw)`, never raw text), so a
33
+ // json field's value here is never source to re-parse — even when it happens to be a plain string
34
+ // (e.g. `default: 'dark'`). The widget owns raw-text parse errors itself via its own `localError`.
35
+ case 'json':
36
+ return null
37
+ case 'datetime': {
38
+ const dt = (field as FieldOf<'datetime'>).options
39
+ const p = dt?.precision ?? 'datetime'
40
+ const re = p === 'date'
41
+ ? /^\d{4}-\d{2}-\d{2}$/
42
+ : p === 'time'
43
+ ? /^\d{2}:\d{2}(:\d{2})?$/
44
+ : /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?$/
45
+ if (dt?.range) {
46
+ const v = value && typeof value === 'object' ? (value as { start?: unknown; end?: unknown }) : {}
47
+ const start = typeof v.start === 'string' ? v.start : ''
48
+ const end = typeof v.end === 'string' ? v.end : ''
49
+ // A picker mid-selection (one endpoint chosen, the other still blank) is invalid regardless of
50
+ // `required` — the server only ever accepts a fully-filled range or a fully-empty one.
51
+ if (!start && !end) return field.required ? 'This field is required.' : null
52
+ if (!start || !end) return 'Both start and end are required.'
53
+ if (!re.test(start)) return 'Invalid start.'
54
+ if (!re.test(end)) return 'Invalid end.'
55
+ if (start > end) return 'Start must be before or equal to end.'
56
+ return null
57
+ }
58
+ return typeof value === 'string' && value && !re.test(value) ? 'Invalid date or time.' : null
59
+ }
60
+ case 'choice': {
61
+ const opts = (field as FieldOf<'choice'>).options
62
+ const allowed = new Set(choiceValues(opts))
63
+ if (opts.multiple) {
64
+ const arr = Array.isArray(value) ? value : []
65
+ if (field.required && arr.length === 0) return 'Select at least one option.'
66
+ if (arr.some((v) => !allowed.has(v as string))) return 'Invalid selection.'
67
+ return null
68
+ }
69
+ if (typeof value === 'string' && !allowed.has(value)) return 'Invalid selection.'
70
+ return null
71
+ }
72
+ default:
73
+ return null
74
+ }
75
+ }
@@ -0,0 +1,7 @@
1
+ export function tryParseJson(text: string): { ok: true; value: unknown } | { ok: false } {
2
+ try {
3
+ return { ok: true, value: JSON.parse(text) }
4
+ } catch {
5
+ return { ok: false }
6
+ }
7
+ }
@@ -0,0 +1,48 @@
1
+ // Internal links inside richtext are stored as a marker href `kestrel:<collection>:<id>` (the editor
2
+ // sets it via the normal Link mark; sanitize allows the `kestrel:` scheme). At read time the marker is
3
+ // rewritten to the target's real localized path — the richtext counterpart of the `link` field's
4
+ // server populator. Pure string helpers, shared by the server populate pass and the client preview.
5
+
6
+ /** URL scheme that flags an internal richtext link (`kestrel:<collection>:<id>`); shared with sanitize. */
7
+ export const RICHTEXT_LINK_SCHEME = 'kestrel'
8
+
9
+ const MARKER_HREF = new RegExp(`href="${RICHTEXT_LINK_SCHEME}:([a-zA-Z0-9_-]+):(\\d+)"`, 'g')
10
+ const MARKER_VALUE = new RegExp(`^${RICHTEXT_LINK_SCHEME}:([a-zA-Z0-9_-]+):(\\d+)$`)
11
+
12
+ /** The marker href the editor stores for an internal richtext link. */
13
+ export function richtextLinkHref(collection: string, id: number): string {
14
+ return `${RICHTEXT_LINK_SCHEME}:${collection}:${id}`
15
+ }
16
+
17
+ /**
18
+ * Parse a bare `kestrel:<collection>:<id>` marker href back to its `{collection, id}` (the inverse of
19
+ * `richtextLinkHref`); returns `null` for an ordinary/external anchor, a malformed marker, or non-string
20
+ * input. Used by the toolbar to prefill the internal-link picker when re-editing an existing link.
21
+ */
22
+ export function parseRichtextLinkHref(href: string | null | undefined): { collection: string; id: number } | null {
23
+ if (typeof href !== 'string') return null
24
+ const m = href.match(MARKER_VALUE)
25
+ return m ? { collection: m[1]!, id: Number(m[2]) } : null
26
+ }
27
+
28
+ /** Every internal-link `{collection, id}` referenced by marker hrefs in the HTML (for batch resolve). */
29
+ export function collectRichtextRefs(html: string | null | undefined): { collection: string; id: number }[] {
30
+ if (!html) return []
31
+ return [...html.matchAll(MARKER_HREF)].map((m) => ({ collection: m[1]!, id: Number(m[2]) }))
32
+ }
33
+
34
+ const escapeAttr = (s: string): string => s.replace(/&/g, '&amp;').replace(/"/g, '&quot;')
35
+
36
+ /**
37
+ * Rewrite every `kestrel:<collection>:<id>` marker href to whatever the injected resolver returns; a
38
+ * target the resolver declines becomes `#`. The production resolver is status-gated, so that covers a
39
+ * missing target AND a draft one. Ordinary anchors are left untouched. Non-string input → `''`.
40
+ */
41
+ export function resolveRichtextLinks(
42
+ html: string | null | undefined,
43
+ resolveHref: (collection: string, id: number) => string | null,
44
+ ): string {
45
+ if (typeof html !== 'string' || !html) return ''
46
+ return html.replace(MARKER_HREF, (_m, collection: string, idStr: string) =>
47
+ `href="${escapeAttr(resolveHref(collection, Number(idStr)) ?? '#')}"`)
48
+ }
@@ -0,0 +1 @@
1
+ export default defineNuxtConfig({})
@@ -0,0 +1,9 @@
1
+ // Admin-only (default-deny applies to /api/**): the per-block field schemas the editor needs to
2
+ // render block inputs. `?allowed=a,b` restricts to a collection's allowed block types.
3
+ export default defineEventHandler((event) => {
4
+ const q = getQuery(event)
5
+ const allowed = typeof q.allowed === 'string' && q.allowed.length
6
+ ? q.allowed.split(',').map((s) => s.trim()).filter(Boolean)
7
+ : undefined
8
+ return { data: allBlocks(allowed).map(serializeBlock) }
9
+ })