@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,401 @@
1
+ <script setup lang="ts">
2
+ import { computed, inject, onMounted, onUnmounted, ref, watch } from 'vue'
3
+ import type { SerializedBlock } from '../../../core/server/utils/serialize-collection'
4
+ import { previewSrc, parseFrameMessage, type PreviewBlockNode } from '../../../public/app/utils/preview-protocol'
5
+ import { createPreviewSender, acceptsFrameEvent, type PreviewSender } from '../utils/preview-channel'
6
+ import { editorFormContextKey } from '../utils/editor-form-context'
7
+ import { PRESETS, matchPreset, fitScale, resolveDim, clampDim, DIM_MIN, WIDTH_MAX, HEIGHT_MAX, type ViewportPreset } from '../utils/preview-viewport'
8
+
9
+ /**
10
+ * The editor half of the live preview: an iframe onto the REAL public page (the record's URL for a
11
+ * saved pageLike record — real CSS, fonts, breakpoints, drafts included via the same-origin admin
12
+ * session — else the dedicated fallback page), kept keystroke-live over the postMessage bridge. The
13
+ * populated tree is pushed to the iframe rAF-coalesced (≤ one frame behind typing, no debounce). Selection syncs both
14
+ * ways: tree click → highlight/scroll in the frame; block click in the frame → `select` up to the tree.
15
+ */
16
+ const props = defineProps<{ content: unknown[]; locale?: string; selectedId?: string | null }>()
17
+ const emit = defineEmits<{ select: [id: string] }>()
18
+ const { t } = useT()
19
+
20
+ // The record's public URL comes from the editor shell (null → fallback page). Injected optionally so
21
+ // the component still mounts standalone (tests, storybook-ish usage) — it then always uses the fallback.
22
+ const ctx = inject(editorFormContextKey, null)
23
+ const src = computed(() => previewSrc(ctx?.previewUrl.value ?? null, props.locale ?? ''))
24
+
25
+ // ---- populate (pipeline: resolve media ids + internal links into reactive caches) ----
26
+ // Block field schemas (non-blocking: byType fills once loaded, then the next send carries the media).
27
+ const { blocks: blockDefs, load } = useBlocks()
28
+ load()
29
+ const byType = computed<Record<string, SerializedBlock>>(() =>
30
+ Object.fromEntries((blockDefs.value ?? []).map((b) => [b.name, b])),
31
+ )
32
+ const items = computed(() => (Array.isArray(props.content) ? props.content : []))
33
+ // `locale` is a getter so a locale change re-resolves media (its cache is per-locale).
34
+ const { ensure: ensureMedia, resolve: resolveMedia } = useMediaResolver(() => props.locale ?? 'en')
35
+ const { ensure: ensureLinks, resolve: resolveLink } = useLinkResolver()
36
+ // The current resolve round. The ready-handshake push awaits it: the frame's first paint is the
37
+ // SERVER-populated page, so pushing a not-yet-resolved client tree would momentarily strip its images/
38
+ // hrefs. With warm (or empty) caches the promise settles immediately — no perceptible delay.
39
+ let ensureRound: Promise<unknown> = Promise.resolve()
40
+ watch(
41
+ [items, byType, () => props.locale],
42
+ ([list, types]) => {
43
+ ensureRound = Promise.allSettled([
44
+ ensureMedia(collectMediaIds(list, types)),
45
+ ensureLinks(collectLinkRefs(list, types)),
46
+ ])
47
+ },
48
+ { immediate: true, deep: true },
49
+ )
50
+ const populated = computed(() =>
51
+ populateBlocksLinks(populateBlocksMedia(items.value, byType.value, resolveMedia), byType.value, resolveLink),
52
+ )
53
+
54
+ // ---- channel (parent side) ----
55
+ const frame = ref<HTMLIFrameElement | null>(null)
56
+ // True once the CURRENT frame document said hello; sends before that are pointless (lost on load).
57
+ const ready = ref(false)
58
+ let sender: PreviewSender | null = null
59
+
60
+ function pushContent() {
61
+ sender?.sendContent(populated.value as PreviewBlockNode[])
62
+ }
63
+
64
+ function onMessage(e: MessageEvent) {
65
+ if (!acceptsFrameEvent(e, frame.value?.contentWindow ?? null, window.location.origin)) return
66
+ const m = parseFrameMessage(e.data)
67
+ if (!m) return
68
+ if (m.kestrel === 'preview:ready') {
69
+ // Fresh document (first load OR reload/src change) — send it the full current state, once the
70
+ // in-flight media/link resolve settles (see ensureRound); `ready` may flip meanwhile (reload).
71
+ ready.value = true
72
+ void ensureRound.then(() => { if (ready.value) pushContent() })
73
+ } else {
74
+ emit('select', m.id)
75
+ }
76
+ }
77
+
78
+ // Every load starts a document that must handshake before it may receive pushes. If consumer script
79
+ // navigated the canvas away (programmatic router push — clicks/submits are already intercepted in the
80
+ // bridge), the loaded document is not the preview page: snap the frame back to the preview URL.
81
+ function onFrameLoad() {
82
+ ready.value = false
83
+ const el = frame.value
84
+ if (!el) return
85
+ try {
86
+ const loc = el.contentWindow?.location
87
+ if (loc && loc.pathname + loc.search !== src.value) el.src = src.value
88
+ } catch {
89
+ el.src = src.value // cross-origin navigation — location unreadable, snap back
90
+ }
91
+ }
92
+
93
+ // Manual fallback for any residual stale component state: reload the iframe. This remounts EVERY
94
+ // component in the frame (not just, say, a stale srcset) and re-handshakes — the fresh document posts
95
+ // `preview:ready`, and onMessage re-pushes the full populated tree after ensureRound. `ready=false`
96
+ // guards pushes to the doc that's tearing down.
97
+ function refresh() {
98
+ ready.value = false
99
+ const el = frame.value
100
+ if (!el) return
101
+ try {
102
+ el.contentWindow?.location.reload() // same URL → onFrameLoad's snap-back check passes
103
+ } catch {
104
+ el.src = src.value // cross-origin: location unreadable → renavigate
105
+ }
106
+ }
107
+
108
+ onMounted(() => {
109
+ sender = createPreviewSender(() => frame.value?.contentWindow ?? null, window.location.origin, () => props.selectedId ?? null)
110
+ window.addEventListener('message', onMessage)
111
+ })
112
+ onUnmounted(() => {
113
+ sender?.dispose()
114
+ sender = null
115
+ window.removeEventListener('message', onMessage)
116
+ })
117
+
118
+ // Keystroke-live: every populated-tree change (edit, media/link cache fill) schedules a send; the
119
+ // channel coalesces bursts to one message per animation frame. Selection posts immediately (tiny).
120
+ watch(populated, () => { if (ready.value) pushContent() })
121
+ watch(() => props.selectedId, (id) => { if (ready.value) sender?.sendSelected(id ?? null) })
122
+ // A src change reloads the iframe (locale switch, first save assigning a URL) → wait for its new ready.
123
+ watch(src, () => { ready.value = false })
124
+
125
+ // ---- responsive preview: device presets (quick-fill) + automatic scale-to-fit + custom W×H ----
126
+ // The iframe renders at the target resolution's REAL px (so the page fires its true breakpoints); a
127
+ // uniform transform only shrinks it visually to fit the pane. Desktop keeps the config-driven breakpoint
128
+ // width but an 'auto' height that fills the pane vertically (the iframe's own document scrolls).
129
+ const previewDesktopWidth = Number((useRuntimeConfig().public as { previewDesktopWidth?: number }).previewDesktopWidth) || 1440
130
+ const presets = PRESETS(previewDesktopWidth)
131
+ const { width, height } = usePreviewViewport(previewDesktopWidth)
132
+ const activePreset = computed(() => matchPreset(width.value, height.value, presets))
133
+
134
+ function selectPreset(p: ViewportPreset) {
135
+ width.value = p.w
136
+ height.value = p.h
137
+ }
138
+ // Commit a W×H input on change (blur/Enter — `change` falls through to the primitive's root <input>), not
139
+ // per keystroke, so the frame doesn't thrash mid-typing. Empty → keep the current value; else full clamp.
140
+ function onDimCommit(target: 'w' | 'h', e: Event) {
141
+ const el = e.target as HTMLInputElement
142
+ if (el.value === '') {
143
+ // A number input also reports '' while it holds un-parsable editing text ('-', '1e'); that must keep
144
+ // the current value, not silently switch the axis. A genuinely EMPTIED field means "auto": the axis
145
+ // fills the pane. (Clearing height under the fixed desktop width re-selects the Desktop preset.)
146
+ if (el.validity?.badInput) return
147
+ if (target === 'w') width.value = 'auto'
148
+ else height.value = 'auto'
149
+ return
150
+ }
151
+ const v = clampDim(Number(el.value), DIM_MIN, target === 'w' ? WIDTH_MAX : HEIGHT_MAX)
152
+ if (v === null) return
153
+ if (target === 'w') width.value = v
154
+ else height.value = v
155
+ }
156
+
157
+ // Both stage axes drive the fit scale — measured with a ResizeObserver (VueUse isn't wired; a manual
158
+ // observer is the in-repo idiom, cf. useTheme's matchMedia). `scale` stays 1 until the first measure.
159
+ const stage = ref<HTMLElement | null>(null)
160
+ const availW = ref(0)
161
+ const availH = ref(0)
162
+ let ro: ResizeObserver | null = null
163
+ const scale = computed(() => fitScale(availW.value, availH.value, width.value, height.value))
164
+ const scalePct = computed(() => Math.round(scale.value * 100))
165
+ // Resolve each Dim to real px: fixed passes through, an 'auto' axis fills the pane at the current scale
166
+ // (falls back to the config width / 900 before measurement so SSR/tests still have a sized frame).
167
+ const frameW = computed(() => resolveDim(availW.value, width.value, scale.value, previewDesktopWidth))
168
+ const frameH = computed(() => resolveDim(availH.value, height.value, scale.value, 900))
169
+
170
+ // The iframe keeps its real target px and is scaled from its top-left; the holder occupies the SCALED
171
+ // footprint so centering behaves. The stage clips (overflow:hidden) so only the iframe document scrolls.
172
+ const frameStyle = computed(() => ({
173
+ width: `${frameW.value}px`,
174
+ height: `${frameH.value}px`,
175
+ transform: `scale(${scale.value})`,
176
+ transformOrigin: 'top left',
177
+ }))
178
+ const viewportStyle = computed(() => ({
179
+ width: `${frameW.value * scale.value}px`,
180
+ height: `${frameH.value * scale.value}px`,
181
+ }))
182
+
183
+ // Content box (padding excluded) so the initial seed matches the ResizeObserver's `contentRect` — both
184
+ // must agree, else the frame footprint would briefly exceed the padded stage (clipped, not scrolled).
185
+ function contentBox(el: HTMLElement) {
186
+ const cs = getComputedStyle(el)
187
+ return {
188
+ width: el.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight),
189
+ height: el.clientHeight - parseFloat(cs.paddingTop) - parseFloat(cs.paddingBottom),
190
+ }
191
+ }
192
+ onMounted(() => {
193
+ const el = stage.value
194
+ if (!el || typeof ResizeObserver === 'undefined') return
195
+ ro = new ResizeObserver((entries) => {
196
+ const b = entries[0]?.contentRect ?? contentBox(el)
197
+ availW.value = b.width
198
+ availH.value = b.height
199
+ })
200
+ ro.observe(el)
201
+ const b = contentBox(el)
202
+ availW.value = b.width
203
+ availH.value = b.height
204
+ })
205
+ onUnmounted(() => { ro?.disconnect(); ro = null })
206
+ </script>
207
+
208
+ <template>
209
+ <section class="block-preview" :aria-label="t('preview.ariaLabel')">
210
+ <div class="block-preview__bar">
211
+ <p class="block-preview__label">{{ t('preview.label') }}</p>
212
+
213
+ <div class="block-preview__tools">
214
+ <div class="block-preview__group">
215
+ <UiTooltip>
216
+ <UiButton
217
+ type="button"
218
+ variant="ghost"
219
+ size="sm"
220
+ icon="rotate-cw"
221
+ :aria-label="t('preview.refresh')"
222
+ @click="refresh"
223
+ />
224
+ <template #content>{{ t('preview.refresh') }}</template>
225
+ </UiTooltip>
226
+ </div>
227
+
228
+ <span class="block-preview__sep" aria-hidden="true" />
229
+
230
+ <div class="block-preview__group" role="group" :aria-label="t('preview.deviceLabel')">
231
+ <UiTooltip v-for="p in presets" :key="p.key">
232
+ <UiButton
233
+ type="button"
234
+ variant="ghost"
235
+ size="sm"
236
+ :icon="p.icon"
237
+ :aria-label="t(p.label)"
238
+ :aria-pressed="activePreset === p.key"
239
+ :class="{ 'block-preview__seg--active': activePreset === p.key }"
240
+ @click="selectPreset(p)"
241
+ />
242
+ <template #content>{{ t(p.label) }} · {{ p.w }} × {{ p.h === 'auto' ? t('preview.auto') : p.h }}</template>
243
+ </UiTooltip>
244
+ </div>
245
+
246
+ <span class="block-preview__sep" aria-hidden="true" />
247
+
248
+ <!-- Read-only: scale is always automatic (two-axis fit), so the % is an indicator, not a control. -->
249
+ <span class="block-preview__pct" aria-hidden="true">{{ scalePct }}%</span>
250
+
251
+ <span class="block-preview__sep" aria-hidden="true" />
252
+
253
+ <div class="block-preview__dims">
254
+ <!-- Either axis can be 'Auto' (fills the pane): typing a number fixes it, clearing the field
255
+ returns it to Auto. Height is Auto under the Desktop preset. -->
256
+ <UiNumberInput
257
+ slim
258
+ class="block-preview__dim"
259
+ :model-value="width === 'auto' ? null : width"
260
+ :min="DIM_MIN"
261
+ :max="WIDTH_MAX"
262
+ :placeholder="width === 'auto' ? t('preview.auto') : undefined"
263
+ :aria-label="t('preview.width')"
264
+ @change="onDimCommit('w', $event)"
265
+ />
266
+ <span class="block-preview__x" aria-hidden="true">×</span>
267
+ <UiNumberInput
268
+ slim
269
+ class="block-preview__dim"
270
+ :model-value="height === 'auto' ? null : height"
271
+ :min="DIM_MIN"
272
+ :max="HEIGHT_MAX"
273
+ :placeholder="height === 'auto' ? t('preview.auto') : undefined"
274
+ :aria-label="t('preview.height')"
275
+ @change="onDimCommit('h', $event)"
276
+ />
277
+ <span class="block-preview__unit" aria-hidden="true">px</span>
278
+ </div>
279
+ </div>
280
+ </div>
281
+
282
+ <div ref="stage" class="block-preview__stage">
283
+ <div class="block-preview__viewport" :style="viewportStyle">
284
+ <iframe ref="frame" class="block-preview__frame" :src="src" :style="frameStyle" :title="t('preview.label')" @load="onFrameLoad" />
285
+ </div>
286
+ </div>
287
+ </section>
288
+ </template>
289
+
290
+ <style lang="scss">
291
+ // The iframe owns rendering + scrolling; this host only frames it and hosts the device toggle.
292
+ .block-preview {
293
+ flex: 1 1 auto;
294
+ min-height: 0;
295
+ display: flex;
296
+ flex-direction: column;
297
+ gap: var(--space-2);
298
+
299
+ &__bar {
300
+ display: flex;
301
+ flex-wrap: wrap;
302
+ align-items: center;
303
+ justify-content: space-between;
304
+ gap: var(--space-2);
305
+ flex: 0 0 auto;
306
+ }
307
+ // Pane heading, folded onto the toolbar row so the preview header is a single thin line (the outer
308
+ // editor pane's own "Preview" label is hidden below to avoid a duplicate).
309
+ &__label {
310
+ margin: 0;
311
+ font-size: var(--text-sm);
312
+ font-weight: var(--weight-medium);
313
+ color: var(--color-text-muted);
314
+ flex: 0 0 auto;
315
+ }
316
+ // Controls cluster to the right of the heading, wrapping among themselves; the whole cluster only
317
+ // wraps below the heading when the middle pane is too narrow to fit both.
318
+ &__tools {
319
+ display: flex;
320
+ flex-wrap: wrap;
321
+ align-items: center;
322
+ justify-content: flex-end;
323
+ gap: var(--space-2);
324
+ flex: 1 1 auto;
325
+ min-width: 0;
326
+ }
327
+ &__group {
328
+ display: flex;
329
+ align-items: center;
330
+ gap: var(--space-1);
331
+ }
332
+ // Active segment: neutral fill + indigo glyph (matches the button-group active idiom).
333
+ &__seg--active {
334
+ color: var(--color-primary);
335
+ background: var(--color-surface-2);
336
+ }
337
+ &__sep {
338
+ align-self: stretch;
339
+ width: 1px;
340
+ min-height: 1.25rem;
341
+ background: var(--color-border);
342
+ }
343
+ &__pct {
344
+ min-width: 3.5ch;
345
+ text-align: right;
346
+ font-size: var(--text-sm);
347
+ color: var(--color-text-muted);
348
+ font-variant-numeric: tabular-nums;
349
+ }
350
+ &__dims {
351
+ display: flex;
352
+ align-items: center;
353
+ gap: var(--space-1);
354
+ font-size: var(--text-sm);
355
+ color: var(--color-text-muted);
356
+ }
357
+ // Compact numeric inputs (the primitive is width:100% by default — override under our wrapper so the
358
+ // selector wins by specificity without touching the shared component). 6rem fits up to 5 typed digits
359
+ // without clipping; flex:none stops the narrow middle pane from shrinking them back below that width.
360
+ &__dims .ui-number {
361
+ width: 6rem;
362
+ flex: none;
363
+ font-variant-numeric: tabular-nums;
364
+ }
365
+
366
+ &__stage {
367
+ flex: 1 1 auto;
368
+ min-height: 0;
369
+ display: flex;
370
+ justify-content: center;
371
+ align-items: center;
372
+ // The container NEVER scrolls: the frame is always scaled to fit both axes, and any over-height
373
+ // content scrolls inside the iframe's own document. Center the (possibly letterboxed) device frame.
374
+ overflow: hidden;
375
+ // No padding: the pane already has its own, and an inset here would misalign the frame's top corners
376
+ // with the rest of the editor's content edge.
377
+
378
+ // Stacked single-column editor (matches the editor3 breakpoint in BlocksBody): the panes become
379
+ // content-sized there, so give the stage a concrete height to measure + scale the frame within.
380
+ @media (max-width: 1100px) {
381
+ height: 70vh;
382
+ }
383
+ }
384
+ // Occupies the SCALED footprint of the frame so centering + overflow behave; owns the crisp 1px border
385
+ // (box-shadow, unscaled). Deliberately square: the preview must read as the RAW page, not a device mock,
386
+ // so no corner radius rounds off the rendered edges. `overflow: hidden` still guards subpixel spill.
387
+ &__viewport {
388
+ flex: 0 0 auto;
389
+ box-shadow: 0 0 0 1px var(--color-border);
390
+ background: #fff;
391
+ overflow: hidden;
392
+ transition: width 160ms ease, height 160ms ease;
393
+ }
394
+ &__frame {
395
+ display: block;
396
+ border: 0;
397
+ background: #fff;
398
+ transition: transform 160ms ease;
399
+ }
400
+ }
401
+ </style>