@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,140 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import FieldRenderer from './Renderer.vue'
4
+ import FieldDeadRefNote from './DeadRefNote.vue'
5
+ import { isFieldVisible } from '../../../../fields/app/utils/condition'
6
+ import { resolveLocalized } from '../../utils/localized'
7
+ import type { FieldDef, Localized } from '../../../../core/server/utils/defineCollection'
8
+ import type { LayoutNode, LayoutTrack } from '../../../../core/server/utils/field-layout'
9
+
10
+ // The ONE shared field-list renderer, reused by every field-list surface (collection page fields, block
11
+ // fields, repeater sub-fields). A FRAGMENT (no wrapper root) so its rows/groups drop straight into the
12
+ // caller's existing gapped flex column — the per-context vertical rhythm is preserved. Imports are
13
+ // explicit (not auto-imported) because this component also renders inside Repeater, which is exercised
14
+ // by the auto-import-free `dom` test runner.
15
+ const props = defineProps<{
16
+ layout?: LayoutNode[]
17
+ fields: Record<string, FieldDef>
18
+ values: Record<string, unknown>
19
+ errors?: Record<string, string>
20
+ /** Field keys holding a stale reference — shown as a non-blocking note under the field. Omit to disable. */
21
+ deadFields?: Set<string>
22
+ locale: string
23
+ disabled?: boolean
24
+ }>()
25
+ const emit = defineEmits<{ update: [name: string, value: unknown] }>()
26
+ const { lang } = useT()
27
+
28
+ // No author layout → the historical one-field-per-row flow, expressed as single-field full-width rows, so
29
+ // both cases flow through a single render path.
30
+ const nodes = computed<LayoutNode[]>(() =>
31
+ props.layout ?? Object.keys(props.fields).map((f) => ({ kind: 'row', fields: [f], tracks: [1] })),
32
+ )
33
+
34
+ // A render-ready row: only the cells that are present + visible, with the grid columns rebuilt from just
35
+ // their tracks. A field hidden by its `condition` must collapse its grid TRACK — otherwise the resolved
36
+ // row would still reserve it and the surviving cells auto-place into the wrong columns. This is the ONE
37
+ // place that turns the engine's neutral tracks (weight numbers / length strings) into CSS-grid columns.
38
+ interface RenderRow { fields: string[]; cols: string }
39
+ type RenderNode = { kind: 'row'; row: RenderRow } | { kind: 'group'; label: Localized; rows: RenderRow[] }
40
+
41
+ const isVisible = (name: string) => !!props.fields[name] && isFieldVisible(props.fields[name]!, props.values)
42
+ const trackToCss = (t: LayoutTrack): string => (typeof t === 'number' ? `${t}fr` : t)
43
+
44
+ function pruneRow(fields: string[], tracks: LayoutTrack[] = []): RenderRow {
45
+ const kept: string[] = []
46
+ const cols: string[] = []
47
+ fields.forEach((name, i) => {
48
+ if (isVisible(name)) { kept.push(name); cols.push(trackToCss(tracks[i] ?? 1)) }
49
+ })
50
+ return { fields: kept, cols: cols.join(' ') || '1fr' }
51
+ }
52
+
53
+ const renderNodes = computed<RenderNode[]>(() =>
54
+ nodes.value.map((node) =>
55
+ node.kind === 'group'
56
+ ? { kind: 'group', label: node.label, rows: node.rows.map((r) => pruneRow(r.fields, r.tracks)) }
57
+ : { kind: 'row', row: pruneRow(node.fields, node.tracks) },
58
+ ),
59
+ )
60
+ </script>
61
+
62
+ <template>
63
+ <template v-for="(node, i) in renderNodes" :key="i">
64
+ <!-- Named group: a labelled fieldset wrapping its stacked rows (one level deep). -->
65
+ <fieldset v-if="node.kind === 'group'" class="ui-field-group">
66
+ <legend class="ui-field-group__legend">{{ resolveLocalized(node.label, lang) }}</legend>
67
+ <div
68
+ v-for="(row, r) in node.rows"
69
+ :key="r"
70
+ class="ui-field-row"
71
+ :style="{ '--ui-field-cols': row.cols }"
72
+ >
73
+ <div v-for="fname in row.fields" :key="fname" class="ui-field-cell">
74
+ <FieldRenderer
75
+ :field="fields[fname]!"
76
+ :name="fname"
77
+ :locale="locale"
78
+ :disabled="disabled"
79
+ :error="errors?.[fname] || null"
80
+ :model-value="values[fname]"
81
+ @update:model-value="(v) => emit('update', fname, v)"
82
+ />
83
+ <FieldDeadRefNote v-if="deadFields" :show="deadFields.has(fname)" />
84
+ </div>
85
+ </div>
86
+ </fieldset>
87
+
88
+ <!-- Plain row: the same grid + cell markup as a group's row, just without the fieldset wrapper. -->
89
+ <div v-else class="ui-field-row" :style="{ '--ui-field-cols': node.row.cols }">
90
+ <div v-for="fname in node.row.fields" :key="fname" class="ui-field-cell">
91
+ <FieldRenderer
92
+ :field="fields[fname]!"
93
+ :name="fname"
94
+ :locale="locale"
95
+ :disabled="disabled"
96
+ :error="errors?.[fname] || null"
97
+ :model-value="values[fname]"
98
+ @update:model-value="(v) => emit('update', fname, v)"
99
+ />
100
+ <FieldDeadRefNote v-if="deadFields" :show="deadFields.has(fname)" />
101
+ </div>
102
+ </div>
103
+ </template>
104
+ </template>
105
+
106
+ <style lang="scss">
107
+ // Grid track set from the CUSTOM PROPERTY (not grid-template-columns directly) so the responsive collapse
108
+ // below — a plain stylesheet rule — wins over the inline value.
109
+ .ui-field-row {
110
+ display: grid;
111
+ grid-template-columns: var(--ui-field-cols, 1fr);
112
+ gap: var(--space-3);
113
+ align-items: start;
114
+ min-inline-size: 0;
115
+ }
116
+ .ui-field-cell {
117
+ display: flex;
118
+ flex-direction: column;
119
+ gap: var(--space-1); // separates a field from its stale-reference note; inert for a single-child cell
120
+ min-inline-size: 0; // let inputs shrink inside a grid track (the repeater idiom)
121
+ }
122
+ .ui-field-group {
123
+ margin: 0;
124
+ padding: 0;
125
+ border: 0;
126
+ display: flex;
127
+ flex-direction: column;
128
+ gap: var(--space-3);
129
+ }
130
+ .ui-field-group__legend {
131
+ padding: 0;
132
+ font-size: var(--text-sm);
133
+ font-weight: var(--weight-medium);
134
+ color: var(--color-text-muted);
135
+ }
136
+ // Responsive collapse to a single column (the repo's existing breakpoint). Container queries are a follow-up.
137
+ @media (max-width: 48rem) {
138
+ .ui-field-row { grid-template-columns: 1fr; }
139
+ }
140
+ </style>
@@ -0,0 +1,112 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import UiField from '../ui/Field.vue'
4
+ import UiButtonGroup from '../ui/ButtonGroup.vue'
5
+ import UiTextInput from '../ui/TextInput.vue'
6
+ import LinkInternalPicker from './LinkInternalPicker.vue'
7
+ import { useLinkField } from '../../composables/useLinkField'
8
+ import type { FieldComponentProps } from '../../utils/field-component'
9
+ import type { FieldOf, LinkType, LinkValue } from '../../../../core/server/utils/defineCollection'
10
+
11
+ const ALL: LinkType[] = ['external', 'email', 'tel', 'internal']
12
+ const TYPE_KEYS: Record<LinkType, string> = {
13
+ internal: 'field.link.type_internal',
14
+ external: 'field.link.type_url',
15
+ email: 'field.link.type_email',
16
+ tel: 'field.link.type_phone',
17
+ }
18
+
19
+ const { t } = useT()
20
+ const props = defineProps<FieldComponentProps>()
21
+ const model = defineModel<LinkValue | null>()
22
+
23
+ const required = computed(() => !!props.field.required)
24
+ const allowed = computed<LinkType[]>(() =>
25
+ props.field.type === 'link' ? ((props.field as FieldOf<'link'>).options?.types ?? ALL) : ALL,
26
+ )
27
+ const typeOptions = computed(() => allowed.value.map((ltype) => ({ value: ltype, label: t(TYPE_KEYS[ltype]) })))
28
+ const internalCollections = computed(() =>
29
+ props.field.type === 'link' ? (props.field as FieldOf<'link'>).options?.collections : undefined,
30
+ )
31
+
32
+ const { currentType, typeModel, url, email, tel, label, collection, recordId, hash } = useLinkField(model, allowed)
33
+ </script>
34
+
35
+ <template>
36
+ <UiField :id="id" :label="name" :error="error" :required="required">
37
+ <template #default="f">
38
+ <UiButtonGroup
39
+ v-if="allowed.length > 1"
40
+ v-model="typeModel"
41
+ :options="typeOptions"
42
+ :disabled="disabled"
43
+ :aria-label="t('field.link.link_type')"
44
+ />
45
+
46
+ <template v-if="currentType === 'external'">
47
+ <UiTextInput
48
+ v-model="url"
49
+ type="url"
50
+ :disabled="disabled"
51
+ :placeholder="t('field.link.url_placeholder')"
52
+ v-bind="f"
53
+ />
54
+ </template>
55
+ <template v-else-if="currentType === 'email'">
56
+ <UiTextInput
57
+ v-model="email"
58
+ type="email"
59
+ :disabled="disabled"
60
+ v-bind="f"
61
+ />
62
+ </template>
63
+ <template v-else-if="currentType === 'tel'">
64
+ <UiTextInput
65
+ v-model="tel"
66
+ type="tel"
67
+ :disabled="disabled"
68
+ v-bind="f"
69
+ />
70
+ </template>
71
+ <template v-else>
72
+ <LinkInternalPicker
73
+ v-model:collection="collection"
74
+ v-model:record-id="recordId"
75
+ :collections="internalCollections"
76
+ :locale="locale"
77
+ :disabled="disabled"
78
+ :input-id="f.id"
79
+ :invalid="f['aria-invalid'] === 'true'"
80
+ :describedby="f['aria-describedby']"
81
+ :required="f.required"
82
+ />
83
+ <UiTextInput
84
+ v-model="hash"
85
+ class="ui-link__hash"
86
+ :disabled="disabled"
87
+ :placeholder="t('field.link.hash_placeholder')"
88
+ :aria-label="t('field.link.hash_placeholder')"
89
+ />
90
+ </template>
91
+
92
+ <span class="ui-link__label-row">
93
+ <UiTextInput
94
+ v-model="label"
95
+ :disabled="disabled"
96
+ :placeholder="t('field.link.label_placeholder')"
97
+ :aria-label="t('field.link.label_placeholder')"
98
+ />
99
+ </span>
100
+ </template>
101
+ </UiField>
102
+ </template>
103
+
104
+ <style lang="scss">
105
+ .ui-link__label-row {
106
+ display: block;
107
+ margin-top: var(--space-1);
108
+ }
109
+ .ui-link__hash {
110
+ margin-top: var(--space-1);
111
+ }
112
+ </style>
@@ -0,0 +1,95 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, onMounted, toRef } from 'vue'
3
+ import UiSelect from '../ui/Select.vue'
4
+ import UiCombobox from '../ui/Combobox.vue'
5
+ import { useRecordOptions } from '../../composables/useRecordOptions'
6
+ import { resolveLocalized } from '../../utils/localized'
7
+ import type { SerializedCollection } from '../../../../core/server/utils/serialize-collection'
8
+
9
+ const { t, lang } = useT()
10
+
11
+ const props = defineProps<{
12
+ collections?: string[]
13
+ locale: string
14
+ disabled?: boolean
15
+ inputId?: string
16
+ invalid?: boolean
17
+ describedby?: string
18
+ required?: boolean
19
+ }>()
20
+
21
+ const collection = defineModel<string | null>('collection')
22
+ const recordId = defineModel<number | null>('recordId')
23
+
24
+ interface CollectionOption { label: string; value: string }
25
+ const collectionOptions = ref<CollectionOption[]>([])
26
+ const showSelect = ref(false) // stays hidden until the fetch resolves → no flash, no empty select
27
+ const loadError = ref<string | null>(null)
28
+
29
+ onMounted(async () => {
30
+ try {
31
+ const res = await $fetch<{ data: SerializedCollection[] }>('/api/collections')
32
+ const filtered = res.data.filter((c) => !props.collections || props.collections.includes(c.name))
33
+ collectionOptions.value = filtered.map((c) => ({
34
+ value: c.name,
35
+ label: resolveLocalized(c.label?.plural, lang.value) ?? resolveLocalized(c.label?.singular, lang.value) ?? c.name,
36
+ }))
37
+ if (collectionOptions.value.length === 1) collection.value ||= collectionOptions.value[0]!.value
38
+ showSelect.value = collectionOptions.value.length > 1
39
+ } catch {
40
+ loadError.value = t('field.linkPicker.loadError')
41
+ }
42
+ })
43
+
44
+ // Reset only on a user pick — a programmatic {collection, id} reseed (record loader) sets both
45
+ // together and must not have its freshly-set id clobbered.
46
+ function onPickCollection(value: string | null | undefined) {
47
+ collection.value = value ?? null
48
+ recordId.value = null
49
+ }
50
+
51
+ const coll = computed(() => collection.value ?? '')
52
+ const ids = computed(() => (recordId.value != null ? [recordId.value] : []))
53
+ const { options, selected, loading, onSearch } = useRecordOptions(coll, ids, toRef(props, 'locale'))
54
+ </script>
55
+
56
+ <template>
57
+ <div class="ui-link-internal">
58
+ <p v-if="loadError" class="ui-link-internal__error">{{ loadError }}</p>
59
+ <UiSelect
60
+ v-if="showSelect"
61
+ :model-value="collection"
62
+ :options="collectionOptions"
63
+ :disabled="disabled"
64
+ :placeholder="t('field.linkPicker.chooseCollection')"
65
+ :aria-label="t('field.linkPicker.collectionLabel')"
66
+ :aria-invalid="invalid || undefined"
67
+ :aria-describedby="describedby"
68
+ @update:model-value="onPickCollection"
69
+ />
70
+ <UiCombobox
71
+ v-model="recordId"
72
+ :options="options"
73
+ :selected="selected"
74
+ :loading="loading"
75
+ :multiple="false"
76
+ :disabled="disabled || !collection"
77
+ :input-id="inputId"
78
+ :invalid="invalid"
79
+ :describedby="describedby"
80
+ :required="required"
81
+ :placeholder="t('field.linkPicker.searchRecords')"
82
+ @search="onSearch"
83
+ />
84
+ </div>
85
+ </template>
86
+
87
+ <style lang="scss">
88
+ // The internal arm stacks two block controls (collection select + record combobox) plus an error line;
89
+ // without this they touched. Matches the `--space-1` rhythm the hash/label rows already use.
90
+ .ui-link-internal {
91
+ display: flex;
92
+ flex-direction: column;
93
+ gap: var(--space-1);
94
+ }
95
+ </style>
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import UiField from '../ui/Field.vue'
4
+ import UiNumberInput from '../ui/NumberInput.vue'
5
+ import { fieldConstraints } from '../../../../fields/app/utils/field-meta'
6
+ import type { FieldComponentProps } from '../../utils/field-component'
7
+ import type { FieldOf } from '../../../../core/server/utils/defineCollection'
8
+
9
+ const props = defineProps<FieldComponentProps>()
10
+ const model = defineModel<number | null>()
11
+ const c = computed(() => fieldConstraints(props.field))
12
+ // Display-only unit suffix (e.g. 'rem'); the stored value stays a bare number.
13
+ const unit = computed(() => (props.field.type === 'number' ? (props.field as FieldOf<'number'>).options?.unit : undefined))
14
+ </script>
15
+
16
+ <template>
17
+ <UiField :id="id" :label="name" :error="error" :required="c.required">
18
+ <template #default="f">
19
+ <UiNumberInput
20
+ v-model="model"
21
+ :min="c.min"
22
+ :max="c.max"
23
+ :step="c.step"
24
+ :suffix="unit"
25
+ :disabled="disabled"
26
+ v-bind="f"
27
+ />
28
+ </template>
29
+ </UiField>
30
+ </template>
@@ -0,0 +1,49 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import UiField from '../ui/Field.vue'
4
+ import UiCombobox from '../ui/Combobox.vue'
5
+ import { useRecordOptions } from '../../composables/useRecordOptions'
6
+ import type { FieldComponentProps } from '../../utils/field-component'
7
+ import type { FieldOf } from '../../../../core/server/utils/defineCollection'
8
+
9
+ const props = defineProps<FieldComponentProps>()
10
+ const model = defineModel<number | number[] | null>()
11
+
12
+ const cfg = computed(() => {
13
+ if (props.field.type !== 'relation') return { collection: '', many: false, label: undefined as string | undefined }
14
+ const r = (props.field as FieldOf<'relation'>).relation
15
+ return { collection: r.collection, many: !!r.many, label: r.labelField }
16
+ })
17
+ const required = computed(() => !!props.field.required)
18
+
19
+ const ids = computed<number[]>(() =>
20
+ cfg.value.many ? (Array.isArray(model.value) ? model.value : []) : (typeof model.value === 'number' ? [model.value] : []))
21
+
22
+ const collection = computed(() => cfg.value.collection)
23
+ const locale = computed(() => props.locale)
24
+ const labelField = computed(() => cfg.value.label)
25
+
26
+ const { options, selected, loading, onSearch } = useRecordOptions(collection, ids, locale, labelField)
27
+ const { t } = useT()
28
+ </script>
29
+
30
+ <template>
31
+ <UiField :id="id" :label="name" :error="error" :required="required">
32
+ <template #default="f">
33
+ <UiCombobox
34
+ v-model="model"
35
+ :options="options"
36
+ :selected="selected"
37
+ :multiple="cfg.many"
38
+ :loading="loading"
39
+ :disabled="disabled"
40
+ :placeholder="t('field.relation.placeholder')"
41
+ :input-id="f.id"
42
+ :invalid="f['aria-invalid'] === 'true'"
43
+ :describedby="f['aria-describedby']"
44
+ :required="f.required"
45
+ @search="onSearch"
46
+ />
47
+ </template>
48
+ </UiField>
49
+ </template>
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { resolveFieldComponent } from '../../utils/field-registry'
4
+ import { resolveLocalized } from '../../utils/localized'
5
+ import { humanizeFieldName } from '../../utils/humanize'
6
+ import FieldUnsupported from './Unsupported.vue'
7
+ import type { FieldComponentProps } from '../../utils/field-component'
8
+
9
+ const props = defineProps<FieldComponentProps>()
10
+ const model = defineModel<unknown>()
11
+ const { lang } = useT()
12
+ const component = computed(() => resolveFieldComponent(props.field.type) ?? FieldUnsupported)
13
+ // The widget renders `name` as its display label. Prefer the def's (localized) label, else humanize the
14
+ // key (`siteName` → "Site Name") so the editor label matches the collection-list header. The field's wire
15
+ // key is owned by the parent, so passing the human label down as `name` is safe.
16
+ const label = computed(() => resolveLocalized(props.field.label, lang.value) ?? humanizeFieldName(props.name))
17
+ </script>
18
+
19
+ <template>
20
+ <component
21
+ :is="component"
22
+ :id="id"
23
+ v-model="model"
24
+ :field="field"
25
+ :name="label"
26
+ :locale="locale"
27
+ :error="error"
28
+ :disabled="disabled"
29
+ />
30
+ </template>