@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,440 @@
1
+ import type { Catalog } from '../composables/useT'
2
+
3
+ // Admin-UI chrome strings (source language + fallback). Keys are dot-namespaced by area. Strings may
4
+ // carry `{name}` placeholders interpolated by `useT().t`. Content (collection data) is NOT translated
5
+ // here — only the dashboard chrome. Keep `en` the complete set; other catalogs may be partial.
6
+ export const en: Catalog = {
7
+ // common / shared
8
+ 'common.new': 'New {label}',
9
+ 'common.save': 'Save',
10
+ 'common.cancel': 'Cancel',
11
+ 'common.create': 'Create',
12
+ 'common.delete': 'Delete',
13
+ 'common.close': 'Close',
14
+ 'common.retry': 'Retry',
15
+
16
+ // navigation rail + shell
17
+ 'nav.dashboard': 'Dashboard',
18
+ 'nav.signOut': 'Sign out',
19
+ 'theme.dark': 'Dark mode',
20
+ 'theme.light': 'Light mode',
21
+ 'a11y.expandSidebar': 'Expand sidebar',
22
+ 'a11y.collapseSidebar': 'Collapse sidebar',
23
+ 'a11y.toLight': 'Switch to light mode',
24
+ 'a11y.toDark': 'Switch to dark mode',
25
+ 'a11y.contentLocale': 'Content locale',
26
+ 'a11y.rowActions': 'Actions',
27
+ 'lang.label': 'Language',
28
+
29
+ // account menu (rail foot)
30
+ 'account.label': 'Account',
31
+ 'account.name': 'Admin',
32
+
33
+ // text input (password reveal)
34
+ 'input.showPassword': 'Show password',
35
+ 'input.hidePassword': 'Hide password',
36
+
37
+ // login
38
+ 'login.signIn': 'Sign in',
39
+ 'login.password': 'Password',
40
+ 'login.invalid': 'Invalid credentials',
41
+ 'login.error': 'Something went wrong — please try again',
42
+
43
+ // dashboard
44
+ 'dash.title': 'Dashboard',
45
+ 'dash.lede': 'Choose a collection to manage its content.',
46
+ 'dash.empty.title': 'No collections yet',
47
+ 'dash.empty.desc': 'Define a collection to start managing content.',
48
+ 'dash.mode.singleton': 'Singleton',
49
+ 'dash.mode.collection': 'Collection',
50
+
51
+ // collection (list page wrapper)
52
+ 'collection.unknown': 'Unknown collection: {name}',
53
+
54
+ // record editor page + toasts
55
+ 'editor.back': '{collection}',
56
+ 'editor.newRecord': 'New {collection}',
57
+ 'editor.editRecord': 'Edit {collection} #{id}',
58
+ 'editor.discardConfirm': 'Discard unsaved changes?',
59
+ 'editor.fixPageFields': 'Please fix the highlighted page fields before saving.',
60
+ 'editor.fixBlockContent': 'Some block content is invalid. Check the highlighted block fields.',
61
+ 'editor.saveConflict': 'This conflicts with an existing record.',
62
+ 'editor.saveFailed': 'Save failed.',
63
+ 'toast.saved': 'Saved',
64
+ 'toast.deleted': 'Deleted',
65
+ 'toast.duplicated': 'Duplicated',
66
+ 'toast.published': 'Published',
67
+ 'toast.unpublished': 'Unpublished',
68
+ 'toast.region': 'Notifications',
69
+ 'toast.dismiss': 'Dismiss',
70
+
71
+ // collection list
72
+ 'list.translations': 'Translations',
73
+ 'list.deadRefs': 'Refs',
74
+ 'list.filter': 'Filter',
75
+ 'list.filterPlaceholder': 'Filter {name}',
76
+ 'list.filterClearAll': 'Clear all filters',
77
+ 'list.removeFilter': 'Remove {name} filter',
78
+ 'list.noFilters': 'No filterable columns.',
79
+ // filter operators (the generic set) + datetime-specific overrides (filter.opDate.*) + boolean values
80
+ 'filter.operator': 'Operator',
81
+ 'filter.op.eq': 'equals',
82
+ 'filter.op.ne': 'not equals',
83
+ 'filter.op.lt': 'less than',
84
+ 'filter.op.lte': 'less or equal',
85
+ 'filter.op.gt': 'greater than',
86
+ 'filter.op.gte': 'greater or equal',
87
+ 'filter.op.contains': 'contains',
88
+ 'filter.op.notContains': 'does not contain',
89
+ 'filter.opDate.lt': 'before',
90
+ 'filter.opDate.lte': 'on or before',
91
+ 'filter.opDate.gt': 'after',
92
+ 'filter.opDate.gte': 'on or after',
93
+ 'filter.bool.true': 'Yes',
94
+ 'filter.bool.false': 'No',
95
+ 'list.columns': 'Columns',
96
+ 'list.columnsReset': 'Reset to defaults',
97
+ 'list.col.id': 'ID',
98
+ 'list.col.slug': 'Slug',
99
+ 'list.col.status': 'Status',
100
+ 'list.col.createdAt': 'Created',
101
+ 'list.col.updatedAt': 'Updated',
102
+ 'list.editLocale': 'Edit {loc}',
103
+ 'list.createLocale': 'Create {loc}',
104
+ 'list.empty.title': 'No records yet',
105
+ 'list.empty.desc': 'Nothing in {name} so far — create the first one.',
106
+ 'list.noMatch.title': 'No matching records',
107
+ 'list.noMatch.desc': 'No {name} match the active filters.',
108
+ 'list.prev': 'Prev',
109
+ 'list.page': 'Page {page} of {totalPages} · {total} total',
110
+ 'list.next': 'Next',
111
+ 'list.perPage': 'Per page',
112
+ 'list.result': '{total} result',
113
+ 'list.results': '{total} results',
114
+ 'list.loadError': "Couldn't load this collection.",
115
+ // row selection + bulk actions
116
+ 'list.selectAll': 'Select all rows on this page',
117
+ 'list.selectRow': 'Select {name}',
118
+ 'list.selected': '{n} selected',
119
+ 'list.clearSelection': 'Clear',
120
+ 'list.rowEdit': 'Edit {name}',
121
+ 'list.rowDuplicate': 'Duplicate {name}',
122
+ 'list.rowDelete': 'Delete {name}',
123
+ 'list.bulkDelete': 'Delete',
124
+ 'list.bulkPublish': 'Publish',
125
+ 'list.bulkUnpublish': 'Unpublish',
126
+ 'list.deleteSummary': 'Delete {n} record(s)? This cannot be undone.',
127
+ 'list.deleteReferrersWarn': '{n} of these are referenced by other records, which will be left with a broken link.',
128
+ 'list.deleteRefsUnverified': 'References could not be verified — some records may be left with a broken link. Proceed with caution.',
129
+ 'list.opFailed': 'The action could not be completed.',
130
+ 'list.refCheckFailed': 'References could not be checked.',
131
+
132
+ // editor preview pane
133
+ 'preview.ariaLabel': 'Live preview',
134
+ 'preview.label': 'Preview',
135
+ 'preview.deviceLabel': 'Preview size',
136
+ 'preview.device.desktop': 'Desktop',
137
+ 'preview.device.tablet': 'Tablet',
138
+ 'preview.device.mobile': 'Mobile',
139
+ 'preview.refresh': 'Refresh preview',
140
+ 'preview.auto': 'Auto',
141
+ 'preview.width': 'Width',
142
+ 'preview.height': 'Height',
143
+
144
+ // page settings (system fields on pageLike collections)
145
+ 'pageSettings.slugLabel': 'Slug',
146
+ 'pageSettings.slugHint': 'URL path, e.g. /about (blank = auto-generated from the title)',
147
+ 'pageSettings.statusLabel': 'Status',
148
+ 'pageSettings.statusHint': 'Draft stays off the live site; Published renders it.',
149
+ 'pageSettings.statusDraft': 'Draft',
150
+ 'pageSettings.statusPublished': 'Published',
151
+
152
+ // editor action bar — live preview + status "Ampel"
153
+ 'editor.openInNewTab': 'Open in new tab',
154
+ // editor Ampel — LEFT lamp: one WORD + a details tooltip (save + publish lifecycle of this record)
155
+ 'editorStatus.word.saving': 'Saving…',
156
+ 'editorStatus.word.unsaved': 'Unsaved',
157
+ 'editorStatus.word.draft': 'Draft',
158
+ 'editorStatus.word.published': 'Published',
159
+ 'editorStatus.word.saved': 'Saved',
160
+ 'editorStatus.detail.saving': 'Saving your changes…',
161
+ 'editorStatus.detail.unsaved': 'You have unsaved local changes.',
162
+ 'editorStatus.detail.draft': 'Saved as a draft — not on the live site.',
163
+ 'editorStatus.detail.published': 'Saved and published.',
164
+ 'editorStatus.detail.saved': 'All changes saved.',
165
+ // editor Ampel — RIGHT lamp: one WORD + a details tooltip (generated state of the static page)
166
+ 'editorStatus.word.live': 'Live',
167
+ 'editorStatus.word.generating': 'Generating…',
168
+ 'editorStatus.word.notBuilt': 'Not built',
169
+ 'editorStatus.word.notLive': 'Not live',
170
+ 'editorStatus.word.error': 'Error',
171
+ 'editorStatus.live.liveS3': 'Live — page uploaded to S3.',
172
+ 'editorStatus.live.liveLocal': 'Live — page written to the static output.',
173
+ 'editorStatus.live.pending': 'Publishing in progress…',
174
+ 'editorStatus.live.notBuilt': 'Not generated in this environment — the static page is built on deploy.',
175
+ 'editorStatus.live.error': 'Publishing the static page failed.',
176
+ 'editorStatus.live.draft': 'Draft — not generated. It stays off the live site until published.',
177
+ 'editorStatus.live.updatedAt': 'Last published {time}',
178
+
179
+ // SEO section + Google preview
180
+ 'seo.sectionLabel': 'SEO',
181
+ 'seo.metaTitle': 'Meta title',
182
+ 'seo.metaDescription': 'Meta description',
183
+ 'seo.noindexLabel': 'Hide this page from search engines (noindex)',
184
+ 'seo.untitled': '(untitled)',
185
+ 'seo.noDescription': 'No meta description — search engines will choose a snippet.',
186
+ 'field.boolean.yes': 'Yes',
187
+ 'field.boolean.no': 'No',
188
+ 'editor.unsupported': 'No editor is registered for type “{type}”.',
189
+ 'seo.socialImage': 'Social share image',
190
+ 'seo.socialImageHint': 'Shown as the preview image when the page is shared (og:image).',
191
+
192
+ // locale bar (translations)
193
+ 'localeBar.fieldLabel': 'Locale',
194
+ 'localeBar.groupLabel': 'Content locale',
195
+ 'localeBar.editLocale': 'Edit {loc}',
196
+ 'localeBar.copyInto': 'Copy {loc} into {current}',
197
+ 'localeBar.createTranslation': 'Create {loc} translation',
198
+ 'localeBar.copyFailed': "Couldn't copy from {locale}.",
199
+
200
+ // editor history (undo/redo)
201
+ 'history.label': 'Edit history',
202
+ 'history.undo': 'Undo',
203
+ 'history.redo': 'Redo',
204
+
205
+ // block editor
206
+ 'blocks.empty': 'No blocks yet.',
207
+ 'blocks.moveUp': 'Move block {n} up',
208
+ 'blocks.moveDown': 'Move block {n} down',
209
+ 'blocks.duplicate': 'Duplicate block {n}',
210
+ 'blocks.remove': 'Remove block {n}',
211
+ 'blocks.add': 'Add block',
212
+ 'blocks.pickType': 'Choose a block type',
213
+ 'blocks.addOfType': 'Add {type}',
214
+ 'blocks.page': 'Page',
215
+ 'blocks.pageFields': 'Page fields',
216
+ 'blocks.invalid': 'Contains errors',
217
+ 'blocks.addInto': 'Add to {slot}',
218
+ 'blocks.noFields': 'This block has no editable fields.',
219
+ 'blocks.fieldsLabel': 'Block fields',
220
+ 'blocks.treeLabel': 'Block structure',
221
+
222
+ // dead references (derived stale-reference warnings)
223
+ 'deadRefs.listWarning': 'Links to a deleted or unpublished record',
224
+ 'deadRefs.blockBadge': 'Broken reference',
225
+ 'deadRefs.fieldNote': 'Points at a deleted or unpublished record.',
226
+
227
+ // broken-references report (the reverse "what links here" view)
228
+ 'refs.title': 'Broken references',
229
+ 'refs.lede': 'Records that link to a deleted or unpublished target. Their published output keeps the stale link until you re-edit them.',
230
+ 'refs.loadError': "Couldn't load the references report — it could not be verified.",
231
+ 'refs.empty.title': 'No broken references',
232
+ 'refs.empty.desc': 'Every reference points at a live, published record.',
233
+ 'refs.colReferrer': 'Record',
234
+ 'refs.colTarget': 'Links to',
235
+ 'refs.colReason': 'Issue',
236
+ 'refs.reason.missing': 'Deleted',
237
+ 'refs.reason.unpublished': 'Unpublished',
238
+ 'refs.dashAlert': '{n} broken reference(s) — review',
239
+
240
+ // field gallery
241
+ 'gallery.title': 'Field gallery',
242
+ 'gallery.lede.before': 'Every implemented field widget, live. The relation and internal-link pickers query ',
243
+ 'gallery.lede.after': ' — create a post or two to see results.',
244
+
245
+ // media library
246
+ 'media.title': 'Media',
247
+ 'media.picker.titleSingle': 'Select a file',
248
+ 'media.picker.titleMultiple': 'Select media',
249
+ // media — right-click context menu
250
+ 'media.menu.cut': 'Cut',
251
+ 'media.menu.copy': 'Copy',
252
+ 'media.menu.paste': 'Paste',
253
+ 'media.menu.pasteInto': 'Paste into folder',
254
+ 'media.menu.rename': 'Rename',
255
+ 'media.menu.delete': 'Delete',
256
+ 'media.menu.deleteN': 'Delete {n} items',
257
+ // media — error fallbacks (the server statusMessage is preferred when present)
258
+ 'media.error.load': 'Failed to load the library',
259
+ 'media.error.relocate': "The operation couldn't be completed.",
260
+ 'media.error.relocateConflict': 'Some items could not be moved or copied.',
261
+ 'media.folderEmpty': 'This folder is empty.',
262
+ 'media.folderNotFound': 'That folder does not exist.',
263
+ 'media.loadMore': 'Load more',
264
+ 'media.clipboardItem': 'item',
265
+ 'media.clipboardItems': 'items',
266
+ 'media.clipboardCut': 'cut',
267
+ 'media.clipboardCopied': 'copied',
268
+ 'media.pasteHere': 'Paste here',
269
+ 'media.clear': 'Clear',
270
+ 'media.uploading': 'Uploading… ',
271
+ 'media.uploadUploaded': 'uploaded',
272
+ 'media.uploadFailed': 'failed',
273
+ 'media.uploadSkipped': 'skipped',
274
+ 'media.uploadError': '{name}: {reason}',
275
+ 'media.uploadFailedReason': 'Upload failed',
276
+ 'media.selected': 'selected',
277
+ 'media.useSelected': 'Use selected',
278
+ 'media.dropToUpload': 'Drop files to upload',
279
+ // media — upload conflict resolution
280
+ 'media.conflicts.title': 'Resolve upload conflicts',
281
+ 'media.conflicts.desc': '{count} file(s) already exist. Choose how to resolve each.',
282
+ 'media.conflicts.applyAll': 'Apply to all:',
283
+ 'media.conflicts.overwriteAll': 'Overwrite all',
284
+ 'media.conflicts.renameAll': 'Rename all',
285
+ 'media.conflicts.skipAll': 'Skip all',
286
+ 'media.conflicts.newName': 'New name for {name}',
287
+ 'media.conflicts.overwrite': 'Overwrite',
288
+ 'media.conflicts.rename': 'Rename',
289
+ 'media.conflicts.skip': 'Skip',
290
+ // media — new folder / rename dialogs
291
+ 'media.newFolder': 'New folder',
292
+ 'media.folderName': 'Folder name',
293
+ 'media.folderNamePlaceholder': 'e.g. holiday',
294
+ 'media.rename': 'Rename',
295
+ 'media.newName': 'New name',
296
+ // media — delete dialog
297
+ 'media.deleteSummary': 'Delete {files} {filesUnit} and {folders} {foldersUnit} — {size}.',
298
+ 'media.file': 'file',
299
+ 'media.files': 'files',
300
+ 'media.folder': 'folder',
301
+ 'media.folders': 'folders',
302
+ 'media.usageWarning': 'These files are used by published content and links will break:',
303
+ // media — move/copy conflict dialog
304
+ 'media.conflictTitleMove': 'Move — conflicts',
305
+ 'media.conflictTitleCopy': 'Copy — conflicts',
306
+ 'media.conflictItems': '{count} item(s) already exist in "{dest}":',
307
+ 'media.rootFolder': 'root',
308
+ 'media.conflictFolderExists': 'folder already exists',
309
+ 'media.conflictFileExists': 'file already exists',
310
+ 'media.skip': 'Skip',
311
+ 'media.replace': 'Replace',
312
+ 'media.keepBoth': 'Keep both',
313
+ // media — table view
314
+ 'media.colName': 'Name',
315
+ 'media.colType': 'Type',
316
+ 'media.colSize': 'Size',
317
+ 'media.colDimensions': 'Dimensions',
318
+ 'media.typeFolder': 'Folder',
319
+ 'media.parentFolder': 'Go to the parent folder',
320
+
321
+ // media — viewer
322
+ 'mediaViewer.folder': 'Folder',
323
+ 'mediaViewer.uploaded': 'Uploaded',
324
+ 'mediaViewer.alt': 'Alt text',
325
+ 'mediaViewer.altHint': 'Describes the image for screen readers and SEO.',
326
+ 'mediaViewer.saveFailed': "Couldn't save the alt text.",
327
+
328
+ // richtext toolbar
329
+ 'richtext.bold': 'Bold',
330
+ 'richtext.italic': 'Italic',
331
+ 'richtext.underline': 'Underline',
332
+ 'richtext.strikethrough': 'Strikethrough',
333
+ 'richtext.inlineCode': 'Inline code',
334
+ 'richtext.highlight': 'Highlight',
335
+ 'richtext.subscript': 'Subscript',
336
+ 'richtext.superscript': 'Superscript',
337
+ 'richtext.bulletList': 'Bullet list',
338
+ 'richtext.orderedList': 'Ordered list',
339
+ 'richtext.blockquote': 'Blockquote',
340
+ 'richtext.codeBlock': 'Code block',
341
+ 'richtext.horizontalRule': 'Horizontal rule',
342
+ 'richtext.alignLeft': 'Align left',
343
+ 'richtext.alignCenter': 'Align center',
344
+ 'richtext.alignRight': 'Align right',
345
+ 'richtext.justify': 'Justify',
346
+ 'richtext.clearFormatting': 'Clear formatting',
347
+ 'richtext.undo': 'Undo',
348
+ 'richtext.redo': 'Redo',
349
+ 'richtext.paragraph': 'Paragraph',
350
+ 'richtext.heading1': 'Heading 1',
351
+ 'richtext.heading2': 'Heading 2',
352
+ 'richtext.heading3': 'Heading 3',
353
+ 'richtext.heading4': 'Heading 4',
354
+ 'richtext.heading5': 'Heading 5',
355
+ 'richtext.heading6': 'Heading 6',
356
+ 'richtext.blockType': 'Block type',
357
+ 'richtext.link': 'Link',
358
+ 'richtext.linkInternal': 'Internal link',
359
+ 'richtext.urlPlaceholder': 'https://…',
360
+ 'richtext.linkUrl': 'Link URL',
361
+ 'richtext.apply': 'Apply',
362
+
363
+ // media toolbar
364
+ 'mediaToolbar.searchPlaceholder': 'Search…',
365
+ 'mediaToolbar.searchAriaLabel': 'Search media',
366
+ 'mediaToolbar.viewGrid': 'Grid',
367
+ 'mediaToolbar.viewTable': 'Table',
368
+ 'mediaToolbar.viewAriaLabel': 'View',
369
+ 'mediaToolbar.upload': 'Upload',
370
+ 'mediaToolbar.newFolder': 'New folder',
371
+
372
+ // media path bar
373
+ 'mediaPath.navAriaLabel': 'Path',
374
+ 'mediaPath.editAriaLabel': 'Edit path',
375
+ 'mediaPath.inputAriaLabel': 'Enter path',
376
+
377
+ // collections nav
378
+ 'nav.collections': 'Collections',
379
+
380
+ // field widgets — datetime
381
+ 'field.datetime.range_start': 'Start',
382
+ 'field.datetime.range_end': 'End',
383
+ // field widgets — link
384
+ 'field.link.type_internal': 'Internal',
385
+ 'field.link.type_url': 'URL',
386
+ 'field.link.type_email': 'Email',
387
+ 'field.link.type_phone': 'Phone',
388
+ 'field.link.link_type': 'Link type',
389
+ 'field.link.url_placeholder': 'https://…',
390
+ 'field.link.label_placeholder': 'Link text (optional)',
391
+ 'field.link.hash_placeholder': 'Anchor (optional, e.g. section-id)',
392
+ // field widgets — relation
393
+ 'field.relation.placeholder': 'Search…',
394
+ // field widgets — internal-link picker
395
+ 'field.linkPicker.loadError': 'Could not load collections.',
396
+ 'field.linkPicker.chooseCollection': 'Choose a collection…',
397
+ 'field.linkPicker.collectionLabel': 'Collection',
398
+ 'field.linkPicker.searchRecords': 'Search records…',
399
+ // field widgets — repeater
400
+ 'field.repeater.moved': 'Moved item to position {n} of {total}',
401
+ 'field.repeater.removed': 'Item {n} removed, {remaining} remaining',
402
+ 'field.repeater.duplicated': 'Duplicated item {n}',
403
+ 'field.repeater.inserted': 'Item inserted at position {n}',
404
+ 'field.repeater.insert_label': 'Insert item at position {n}',
405
+ 'field.repeater.item_label': 'Item {n}',
406
+ 'field.repeater.move_up': 'Move item {n} up',
407
+ 'field.repeater.move_down': 'Move item {n} down',
408
+ 'field.repeater.duplicate_label': 'Duplicate item {n}',
409
+ 'field.repeater.remove_label': 'Remove item {n}',
410
+ 'field.repeater.empty': 'No items yet.',
411
+ 'field.repeater.add': 'Add item',
412
+ // field widgets — media
413
+ 'field.media.empty': 'No media selected.',
414
+ 'field.media.add': 'Add media',
415
+ 'field.media.replace': 'Replace',
416
+ 'field.media.select': 'Select media',
417
+ 'field.media.remove': 'Remove {name}',
418
+ 'field.media.itemAt': 'media #{pos}',
419
+ 'field.media.move_earlier': 'Move {name} earlier',
420
+ 'field.media.move_later': 'Move {name} later',
421
+ 'field.media.moved': 'Moved {name} to position {pos} of {total}',
422
+
423
+ // combobox
424
+ 'combobox.moved': 'Moved {label} to position {pos} of {total}',
425
+ 'combobox.move_earlier': 'Move {label} earlier',
426
+ 'combobox.move_later': 'Move {label} later',
427
+ 'combobox.remove': 'Remove {label}',
428
+ 'combobox.clear': 'Clear',
429
+ 'combobox.toggle_options': 'Toggle options',
430
+ 'combobox.loading': 'Loading…',
431
+ 'combobox.no_results': 'No results',
432
+
433
+ // date pickers
434
+ 'datePicker.openCalendar': 'Open calendar',
435
+ 'datePicker.previousMonth': 'Previous month',
436
+ 'datePicker.nextMonth': 'Next month',
437
+ 'dateRange.openCalendar': 'Open calendar',
438
+ 'dateRange.previousMonth': 'Previous month',
439
+ 'dateRange.nextMonth': 'Next month',
440
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Map a ButtonGroup (single-select ToggleGroup) selection to the boolean model. A deselect — reka emits
3
+ * null/undefined when the already-active option is re-clicked — keeps the current value instead of
4
+ * coercing to `false`, so re-clicking the selected option is a no-op rather than a silent inversion.
5
+ */
6
+ export function nextBooleanValue(current: boolean | undefined, incoming: string | string[] | null): boolean | undefined {
7
+ if (incoming === 'true') return true
8
+ if (incoming === 'false') return false
9
+ return current
10
+ }
@@ -0,0 +1,22 @@
1
+ import type { FieldDef } from '../../../core/server/utils/defineCollection'
2
+
3
+ /** A record reference: its id plus a human label. Shared by UiCombobox and FieldRelation. */
4
+ export interface FieldOption { value: number; label: string }
5
+
6
+ /**
7
+ * The shared prop contract every Field* widget honors (plus a v-model).
8
+ *
9
+ * Validity vocabulary (two intentional names, one seam): `error: string | null` is the validation
10
+ * MESSAGE, carried by the wrappers (UiField/UiFieldset) which own its display + aria-describedby. A few
11
+ * portaled controls that render outside the wrapper (UiCombobox, FieldLinkInternalPicker) also take an
12
+ * `invalid: boolean` — just the visual ring — because they can't see the wrapper's `[data-invalid]`.
13
+ * So: `error` = the message at the field boundary; `invalid` = the ring for detached/teleported controls.
14
+ */
15
+ export interface FieldComponentProps {
16
+ field: FieldDef
17
+ name: string
18
+ locale: string
19
+ error?: string | null
20
+ disabled?: boolean
21
+ id?: string
22
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Per-type blank-value registry for consumer-defined field types — consulted by the editor's form-init
3
+ * (`emptyForField` in admin/edit-form). A custom array/object-backed type registers its empty here so a
4
+ * NEW record / block seeds the right shape instead of `null`. Client-side + co-located with
5
+ * `registerFieldComponent` (form-init is a client concern; the server descriptor can't reach it). No Vue
6
+ * imports, so it is safe to import from the node-tested edit-form util.
7
+ *
8
+ * @example
9
+ * // app/plugins/field-types.client.ts
10
+ * registerFieldComponent('tags', TagsField)
11
+ * registerFieldEmpty('tags', () => []) // a new record seeds [] instead of null
12
+ */
13
+ const fieldEmpties: Record<string, () => unknown> = {}
14
+
15
+ export function registerFieldEmpty(type: string, make: () => unknown): void {
16
+ fieldEmpties[type] = make
17
+ }
18
+
19
+ export function resolveFieldEmpty(type: string): (() => unknown) | undefined {
20
+ return fieldEmpties[type]
21
+ }
@@ -0,0 +1,36 @@
1
+ import { defineAsyncComponent, type Component } from 'vue'
2
+ import type { FieldType } from '../../../core/server/utils/defineCollection'
3
+ import FieldText from '../components/field/Text.vue'
4
+ import FieldNumber from '../components/field/Number.vue'
5
+ import FieldBoolean from '../components/field/Boolean.vue'
6
+ import FieldJson from '../components/field/Json.vue'
7
+ import FieldChoice from '../components/field/Choice.vue'
8
+ import FieldRelation from '../components/field/Relation.vue'
9
+ import FieldLink from '../components/field/Link.vue'
10
+ import FieldSlug from '../components/field/Slug.vue'
11
+
12
+ /** Field types with a widget so far. Each later slice adds its entry. */
13
+ export const fieldComponents: Partial<Record<FieldType, Component>> = {
14
+ text: FieldText,
15
+ slug: FieldSlug,
16
+ number: FieldNumber,
17
+ boolean: FieldBoolean,
18
+ json: FieldJson,
19
+ choice: FieldChoice,
20
+ relation: FieldRelation,
21
+ link: FieldLink,
22
+ // Async-loaded so their heavy deps never enter the base editor chunk — they load only when a field of
23
+ // that type actually renders. richtext pulls in all of TipTap (the single heaviest dep group); datetime
24
+ // pulls reka-ui + @internationalized/date.
25
+ datetime: defineAsyncComponent(() => import('../components/field/Datetime.vue')),
26
+ richtext: defineAsyncComponent(() => import('../components/field/Richtext.vue')),
27
+ // Async-loaded to break the registry → Repeater → Renderer → registry import cycle.
28
+ repeater: defineAsyncComponent(() => import('../components/field/Repeater.vue')),
29
+ }
30
+
31
+ export const resolveFieldComponent = (type: FieldType): Component | undefined => fieldComponents[type]
32
+
33
+ /** Register a field widget for a type (domain layers call this from a plugin). Mutates the singleton. */
34
+ export function registerFieldComponent(type: FieldType, component: Component): void {
35
+ fieldComponents[type] = component
36
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Turn a field key into a human label: split snake_case / kebab-case / camelCase into words and
3
+ * capitalize the first. The shared fallback when a field has no explicit (localized) `label`, used by
4
+ * both the editor field widgets (via the FieldRenderer) and the collection-list column headers — so the
5
+ * editor label and the list header read the same.
6
+ */
7
+ export function humanizeFieldName(name: string): string {
8
+ const spaced = name
9
+ .replace(/[_-]+/g, ' ')
10
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
11
+ .replace(/\s+/g, ' ')
12
+ .trim()
13
+ return spaced ? spaced.charAt(0).toUpperCase() + spaced.slice(1) : name
14
+ }