@michaelthielemann/kestrel 4.1.3 → 5.0.2

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 (342) hide show
  1. package/README.md +26 -201
  2. package/dist/boot.d.ts +45 -0
  3. package/dist/boot.js +284 -0
  4. package/dist/cast.d.ts +3 -0
  5. package/dist/cast.js +6 -0
  6. package/dist/catalogue.d.ts +11 -0
  7. package/dist/catalogue.js +1 -0
  8. package/dist/cli.d.ts +2 -0
  9. package/dist/cli.js +55 -0
  10. package/dist/context.d.ts +62 -0
  11. package/dist/context.js +61 -0
  12. package/dist/dataflow.d.ts +3 -0
  13. package/dist/dataflow.js +65 -0
  14. package/dist/defineConfig.d.ts +162 -0
  15. package/dist/defineConfig.js +42 -0
  16. package/dist/defineContract.d.ts +12 -0
  17. package/dist/defineContract.js +16 -0
  18. package/dist/defineModule.d.ts +75 -0
  19. package/dist/defineModule.js +9 -0
  20. package/dist/definePipeline.d.ts +7 -0
  21. package/dist/definePipeline.js +11 -0
  22. package/dist/errors.d.ts +26 -0
  23. package/dist/errors.js +48 -0
  24. package/dist/index.d.ts +19 -0
  25. package/dist/index.js +14 -0
  26. package/dist/load.d.ts +7 -0
  27. package/dist/load.js +79 -0
  28. package/dist/logger.d.ts +18 -0
  29. package/dist/logger.js +36 -0
  30. package/dist/registry.d.ts +14 -0
  31. package/dist/registry.js +87 -0
  32. package/dist/result.d.ts +19 -0
  33. package/dist/result.js +19 -0
  34. package/dist/runner.d.ts +29 -0
  35. package/dist/runner.js +133 -0
  36. package/dist/schema.d.ts +7 -0
  37. package/dist/schema.js +142 -0
  38. package/dist/sort.d.ts +2 -0
  39. package/dist/sort.js +50 -0
  40. package/dist/testing/runPipeline.d.ts +9 -0
  41. package/dist/testing/runPipeline.js +22 -0
  42. package/dist/triggers/allowlist.d.ts +9 -0
  43. package/dist/triggers/allowlist.js +49 -0
  44. package/dist/triggers/cron.d.ts +10 -0
  45. package/dist/triggers/cron.js +44 -0
  46. package/dist/triggers/cronMatch.d.ts +10 -0
  47. package/dist/triggers/cronMatch.js +68 -0
  48. package/dist/triggers/http.d.ts +59 -0
  49. package/dist/triggers/http.js +300 -0
  50. package/package.json +25 -151
  51. package/NOTICE +0 -12
  52. package/kestrel.config.ts +0 -28
  53. package/layers/access/nuxt.config.ts +0 -5
  54. package/layers/access/server/middleware/00.ip-allowlist.ts +0 -52
  55. package/layers/access/server/middleware/access-guard.ts +0 -32
  56. package/layers/access/server/types.d.ts +0 -7
  57. package/layers/admin/app/components/AdminAccount.vue +0 -212
  58. package/layers/admin/app/components/AdminNav.vue +0 -44
  59. package/layers/admin/app/components/BlockFields.vue +0 -67
  60. package/layers/admin/app/components/BlockPreview.vue +0 -413
  61. package/layers/admin/app/components/BlockTree.vue +0 -433
  62. package/layers/admin/app/components/BlocksBody.vue +0 -163
  63. package/layers/admin/app/components/CollectionDeleteDialog.vue +0 -38
  64. package/layers/admin/app/components/CollectionEditor.vue +0 -324
  65. package/layers/admin/app/components/CollectionList.vue +0 -429
  66. package/layers/admin/app/components/CollectionListBulkBar.vue +0 -62
  67. package/layers/admin/app/components/CollectionListColumnsPanel.vue +0 -37
  68. package/layers/admin/app/components/CollectionListFilterPanel.vue +0 -145
  69. package/layers/admin/app/components/CollectionListPager.vue +0 -70
  70. package/layers/admin/app/components/CollectionListTable.vue +0 -347
  71. package/layers/admin/app/components/EditorStatus.vue +0 -211
  72. package/layers/admin/app/components/EditorUnsupported.vue +0 -28
  73. package/layers/admin/app/components/FieldsBody.vue +0 -23
  74. package/layers/admin/app/components/LocaleBar.vue +0 -107
  75. package/layers/admin/app/components/PageFields.vue +0 -129
  76. package/layers/admin/app/components/PageFieldsPane.vue +0 -39
  77. package/layers/admin/app/components/SeoFields.vue +0 -210
  78. package/layers/admin/app/components/SingletonEditor.vue +0 -93
  79. package/layers/admin/app/composables/useAuth.ts +0 -68
  80. package/layers/admin/app/composables/useBlockTree.ts +0 -95
  81. package/layers/admin/app/composables/useBlocks.ts +0 -7
  82. package/layers/admin/app/composables/useCachedResource.ts +0 -35
  83. package/layers/admin/app/composables/useCollectionOps.ts +0 -102
  84. package/layers/admin/app/composables/useCollections.ts +0 -6
  85. package/layers/admin/app/composables/useContentLocales.ts +0 -23
  86. package/layers/admin/app/composables/useEditForm.ts +0 -402
  87. package/layers/admin/app/composables/useLinkResolver.ts +0 -40
  88. package/layers/admin/app/composables/useListBatchActions.ts +0 -78
  89. package/layers/admin/app/composables/useListColumns.ts +0 -53
  90. package/layers/admin/app/composables/useListFilterDraft.ts +0 -129
  91. package/layers/admin/app/composables/useListRows.ts +0 -75
  92. package/layers/admin/app/composables/useListSelection.ts +0 -28
  93. package/layers/admin/app/composables/useListUrlState.ts +0 -53
  94. package/layers/admin/app/composables/usePendingPublishPoll.ts +0 -15
  95. package/layers/admin/app/composables/usePreviewViewport.ts +0 -30
  96. package/layers/admin/app/composables/usePublishStatus.ts +0 -107
  97. package/layers/admin/app/composables/useRailCollapsed.ts +0 -14
  98. package/layers/admin/app/composables/useTheme.ts +0 -36
  99. package/layers/admin/app/composables/useToolbarPanel.ts +0 -38
  100. package/layers/admin/app/composables/useUnsavedGuard.ts +0 -22
  101. package/layers/admin/app/layouts/admin.vue +0 -266
  102. package/layers/admin/app/middleware/admin-auth.ts +0 -4
  103. package/layers/admin/app/pages/admin/[collection]/[id].vue +0 -203
  104. package/layers/admin/app/pages/admin/[collection]/index.vue +0 -62
  105. package/layers/admin/app/pages/admin/index.vue +0 -118
  106. package/layers/admin/app/pages/admin/login.vue +0 -68
  107. package/layers/admin/app/pages/admin/media.vue +0 -28
  108. package/layers/admin/app/pages/admin/references.vue +0 -146
  109. package/layers/admin/app/plugins/admin-style-guard.client.ts +0 -76
  110. package/layers/admin/app/plugins/reauth.client.ts +0 -20
  111. package/layers/admin/app/utils/admin-style-guard.ts +0 -20
  112. package/layers/admin/app/utils/block-tree.ts +0 -214
  113. package/layers/admin/app/utils/collection-ops.ts +0 -69
  114. package/layers/admin/app/utils/dead-refs.ts +0 -30
  115. package/layers/admin/app/utils/edit-form.ts +0 -267
  116. package/layers/admin/app/utils/editor-expose.ts +0 -36
  117. package/layers/admin/app/utils/editor-form-context.ts +0 -82
  118. package/layers/admin/app/utils/editor-registry.ts +0 -20
  119. package/layers/admin/app/utils/field-keys.ts +0 -11
  120. package/layers/admin/app/utils/list-cell.ts +0 -46
  121. package/layers/admin/app/utils/list-columns.ts +0 -142
  122. package/layers/admin/app/utils/list-query.ts +0 -96
  123. package/layers/admin/app/utils/nav-active.ts +0 -9
  124. package/layers/admin/app/utils/populate-blocks.ts +0 -288
  125. package/layers/admin/app/utils/preview-channel.ts +0 -62
  126. package/layers/admin/app/utils/preview-viewport.ts +0 -80
  127. package/layers/admin/app/utils/reauth.ts +0 -67
  128. package/layers/admin/app/utils/record-title.ts +0 -15
  129. package/layers/admin/app/utils/register-builtin-editors.ts +0 -9
  130. package/layers/admin/app/utils/safe-redirect.ts +0 -12
  131. package/layers/admin/nuxt.config.ts +0 -20
  132. package/layers/auth/nuxt.config.ts +0 -1
  133. package/layers/auth/server/plugins/01.register-auth-pipelines.ts +0 -7
  134. package/layers/collections/nuxt.config.ts +0 -1
  135. package/layers/collections/server/plugins/01.register.ts +0 -29
  136. package/layers/collections/server/plugins/02.register-relation-populate.ts +0 -18
  137. package/layers/core/app/composables/layouts.ts +0 -5
  138. package/layers/core/app/utils/field-factories.ts +0 -129
  139. package/layers/core/app/utils/layouts.ts +0 -38
  140. package/layers/core/modules/auto-discovery/extract-block.ts +0 -179
  141. package/layers/core/modules/auto-discovery/index.ts +0 -112
  142. package/layers/core/modules/auto-discovery/package-registry.ts +0 -19
  143. package/layers/core/modules/auto-discovery/scan.ts +0 -170
  144. package/layers/core/modules/auto-discovery/virtual.d.ts +0 -20
  145. package/layers/core/modules/component-namespace/index.ts +0 -44
  146. package/layers/core/modules/component-namespace/shared.ts +0 -24
  147. package/layers/core/modules/kestrel/app-shell.ts +0 -55
  148. package/layers/core/modules/kestrel/auto-imports.ts +0 -44
  149. package/layers/core/modules/kestrel/index.ts +0 -144
  150. package/layers/core/modules/plugin-order/index.ts +0 -69
  151. package/layers/core/modules/plugin-order/plugin-order.ts +0 -273
  152. package/layers/core/nuxt.config.ts +0 -10
  153. package/layers/core/server/api/[...path].ts +0 -98
  154. package/layers/core/server/plugins/00.config.ts +0 -12
  155. package/layers/core/server/plugins/00.migrate.ts +0 -12
  156. package/layers/core/server/plugins/01.register-introspection-pipeline.ts +0 -5
  157. package/layers/core/server/plugins/01.register-openapi-pipeline.ts +0 -5
  158. package/layers/core/server/plugins/01.register-tooling-pipelines.ts +0 -5
  159. package/layers/core/server/plugins/02.schema-sync.ts +0 -58
  160. package/layers/core/server/plugins/04.outbox-worker.ts +0 -31
  161. package/layers/core/server/plugins/05.reindex-refs.ts +0 -5
  162. package/layers/core/server/routes/__kestrel/dashboard.get.ts +0 -29
  163. package/layers/core/server/tasks/db/migrate-module.ts +0 -83
  164. package/layers/core/server/tasks/db/migrate-revisions.ts +0 -20
  165. package/layers/core/server/tasks/db/migrate.ts +0 -50
  166. package/layers/core/server/utils/server-config.ts +0 -81
  167. package/layers/fields/nuxt.config.ts +0 -1
  168. package/layers/fields/server/plugins/01.register-blocks-pipeline.ts +0 -7
  169. package/layers/fields/server/plugins/01.register-field-populate.ts +0 -11
  170. package/layers/media/app/components/KestrelImg.vue +0 -94
  171. package/layers/media/app/components/MediaConflictDialog.vue +0 -35
  172. package/layers/media/app/components/MediaDeleteDialog.vue +0 -52
  173. package/layers/media/app/components/MediaGrid.vue +0 -113
  174. package/layers/media/app/components/MediaImage.vue +0 -70
  175. package/layers/media/app/components/MediaLibrary.vue +0 -456
  176. package/layers/media/app/components/MediaNewFolderDialog.vue +0 -32
  177. package/layers/media/app/components/MediaPathBar.vue +0 -163
  178. package/layers/media/app/components/MediaPicker.vue +0 -19
  179. package/layers/media/app/components/MediaRenameDialog.vue +0 -32
  180. package/layers/media/app/components/MediaTable.vue +0 -75
  181. package/layers/media/app/components/MediaThumb.vue +0 -25
  182. package/layers/media/app/components/MediaToolbar.vue +0 -90
  183. package/layers/media/app/components/MediaUploadDialog.vue +0 -87
  184. package/layers/media/app/components/MediaViewer.vue +0 -137
  185. package/layers/media/app/components/field/Media.vue +0 -263
  186. package/layers/media/app/composables/useBlurUp.ts +0 -55
  187. package/layers/media/app/composables/useMediaClipboard.ts +0 -14
  188. package/layers/media/app/composables/useMediaContextMenu.ts +0 -56
  189. package/layers/media/app/composables/useMediaDnd.ts +0 -56
  190. package/layers/media/app/composables/useMediaLibrary.ts +0 -152
  191. package/layers/media/app/composables/useMediaOps.ts +0 -50
  192. package/layers/media/app/composables/useMediaRelocate.ts +0 -39
  193. package/layers/media/app/composables/useMediaResolver.ts +0 -50
  194. package/layers/media/app/composables/useMediaUpload.ts +0 -93
  195. package/layers/media/app/composables/useMediaVariant.ts +0 -34
  196. package/layers/media/app/plugins/field-media.client.ts +0 -6
  197. package/layers/media/app/utils/ai-disclosure.ts +0 -19
  198. package/layers/media/app/utils/dnd.ts +0 -69
  199. package/layers/media/app/utils/library.ts +0 -81
  200. package/layers/media/app/utils/ops.ts +0 -60
  201. package/layers/media/app/utils/picture-model.ts +0 -125
  202. package/layers/media/app/utils/thumbhash.ts +0 -19
  203. package/layers/media/app/utils/variant-name.ts +0 -20
  204. package/layers/media/nuxt.config.ts +0 -7
  205. package/layers/media/server/middleware/ondemand-variants.ts +0 -38
  206. package/layers/media/server/plugins/01.register-media-pipelines.ts +0 -7
  207. package/layers/media/server/plugins/02.register-media.ts +0 -22
  208. package/layers/media/server/plugins/04.variant-capture.ts +0 -27
  209. package/layers/media/server/plugins/05.media-cleanup.ts +0 -5
  210. package/layers/media/server/tasks/media/backfill.ts +0 -17
  211. package/layers/public/app/app.vue +0 -5
  212. package/layers/public/app/components/BlockRenderer.vue +0 -56
  213. package/layers/public/app/components/KestrelLink.vue +0 -26
  214. package/layers/public/app/components/KestrelPreviewBridge.vue +0 -133
  215. package/layers/public/app/composables/public-page.ts +0 -24
  216. package/layers/public/app/error.vue +0 -12
  217. package/layers/public/app/layouts/default.vue +0 -5
  218. package/layers/public/app/pages/[...slug].vue +0 -239
  219. package/layers/public/app/pages/__kestrel/preview.vue +0 -46
  220. package/layers/public/app/utils/block-edit-context.ts +0 -16
  221. package/layers/public/app/utils/json-ld.ts +0 -139
  222. package/layers/public/app/utils/link-href.ts +0 -46
  223. package/layers/public/app/utils/page-head.ts +0 -84
  224. package/layers/public/app/utils/page-layout.ts +0 -18
  225. package/layers/public/app/utils/preview-protocol.ts +0 -117
  226. package/layers/public/app/utils/public-route.ts +0 -46
  227. package/layers/public/app/utils/site-head.ts +0 -43
  228. package/layers/public/modules/deploy-output/index.ts +0 -80
  229. package/layers/public/modules/prerender-routes/discover.ts +0 -37
  230. package/layers/public/modules/prerender-routes/index.ts +0 -88
  231. package/layers/public/modules/prerender-routes/routes.ts +0 -16
  232. package/layers/public/modules/prune-media/index.ts +0 -48
  233. package/layers/public/modules/prune-media/prune-media.ts +0 -81
  234. package/layers/public/nuxt.config.ts +0 -14
  235. package/layers/public/server/delivery-live/serve.ts +0 -111
  236. package/layers/public/server/middleware/delivery-live-catchall.ts +0 -7
  237. package/layers/public/server/pipelines/index.ts +0 -23
  238. package/layers/public/server/plugins/00.ensure-snapshot-triggers.ts +0 -14
  239. package/layers/public/server/plugins/01.register-public-pipelines.ts +0 -7
  240. package/layers/public/server/plugins/02.register-links.ts +0 -16
  241. package/layers/public/server/plugins/03.redirects.ts +0 -55
  242. package/layers/public/server/plugins/05.plan-publish.ts +0 -5
  243. package/layers/public/server/plugins/zz.publish.ts +0 -127
  244. package/layers/public/server/routes/llms-full.txt.get.ts +0 -99
  245. package/layers/public/server/routes/llms.txt.get.ts +0 -66
  246. package/layers/public/server/routes/redirects.json.get.ts +0 -58
  247. package/layers/public/server/routes/robots.txt.get.ts +0 -11
  248. package/layers/public/server/routes/sitemap.xml.get.ts +0 -79
  249. package/layers/public/server/tasks/publish/run.ts +0 -32
  250. package/layers/public/server/utils/publish/render-live.ts +0 -26
  251. package/layers/ui/app/assets/scss/_base.scss +0 -16
  252. package/layers/ui/app/assets/scss/_datepicker.scss +0 -82
  253. package/layers/ui/app/assets/scss/_mixins.scss +0 -50
  254. package/layers/ui/app/assets/scss/_reset.scss +0 -65
  255. package/layers/ui/app/assets/scss/_tokens.scss +0 -183
  256. package/layers/ui/app/assets/scss/main.scss +0 -4
  257. package/layers/ui/app/components/field/Boolean.vue +0 -30
  258. package/layers/ui/app/components/field/Choice.vue +0 -73
  259. package/layers/ui/app/components/field/Datetime.vue +0 -98
  260. package/layers/ui/app/components/field/DeadRefNote.vue +0 -28
  261. package/layers/ui/app/components/field/Json.vue +0 -62
  262. package/layers/ui/app/components/field/Layout.vue +0 -138
  263. package/layers/ui/app/components/field/Link.vue +0 -112
  264. package/layers/ui/app/components/field/LinkInternalPicker.vue +0 -95
  265. package/layers/ui/app/components/field/Number.vue +0 -30
  266. package/layers/ui/app/components/field/Relation.vue +0 -49
  267. package/layers/ui/app/components/field/Renderer.vue +0 -30
  268. package/layers/ui/app/components/field/Repeater.vue +0 -460
  269. package/layers/ui/app/components/field/Richtext.vue +0 -26
  270. package/layers/ui/app/components/field/Slug.vue +0 -65
  271. package/layers/ui/app/components/field/Text.vue +0 -35
  272. package/layers/ui/app/components/field/Unsupported.vue +0 -34
  273. package/layers/ui/app/components/ui/Alert.vue +0 -50
  274. package/layers/ui/app/components/ui/Brand.vue +0 -22
  275. package/layers/ui/app/components/ui/Button.vue +0 -116
  276. package/layers/ui/app/components/ui/ButtonGroup.vue +0 -96
  277. package/layers/ui/app/components/ui/Checkbox.vue +0 -53
  278. package/layers/ui/app/components/ui/CheckboxGroup.vue +0 -51
  279. package/layers/ui/app/components/ui/Combobox.vue +0 -345
  280. package/layers/ui/app/components/ui/DatePicker.vue +0 -92
  281. package/layers/ui/app/components/ui/DateRangePicker.vue +0 -100
  282. package/layers/ui/app/components/ui/Dialog.vue +0 -107
  283. package/layers/ui/app/components/ui/EmptyState.vue +0 -46
  284. package/layers/ui/app/components/ui/Field.vue +0 -58
  285. package/layers/ui/app/components/ui/Fieldset.vue +0 -65
  286. package/layers/ui/app/components/ui/Icon.vue +0 -44
  287. package/layers/ui/app/components/ui/Menu.vue +0 -54
  288. package/layers/ui/app/components/ui/NumberInput.vue +0 -175
  289. package/layers/ui/app/components/ui/Richtext.vue +0 -141
  290. package/layers/ui/app/components/ui/RichtextToolbar.vue +0 -251
  291. package/layers/ui/app/components/ui/Select.vue +0 -52
  292. package/layers/ui/app/components/ui/TextInput.vue +0 -111
  293. package/layers/ui/app/components/ui/Textarea.vue +0 -39
  294. package/layers/ui/app/components/ui/TimeInput.vue +0 -18
  295. package/layers/ui/app/components/ui/Toasts.vue +0 -103
  296. package/layers/ui/app/components/ui/Tooltip.vue +0 -45
  297. package/layers/ui/app/composables/useAdminLang.ts +0 -8
  298. package/layers/ui/app/composables/useDragReorder.ts +0 -41
  299. package/layers/ui/app/composables/useEchoGuard.ts +0 -28
  300. package/layers/ui/app/composables/useLinkField.ts +0 -70
  301. package/layers/ui/app/composables/useRecordOptions.ts +0 -84
  302. package/layers/ui/app/composables/useRepeater.ts +0 -99
  303. package/layers/ui/app/composables/useT.ts +0 -32
  304. package/layers/ui/app/composables/useToast.ts +0 -46
  305. package/layers/ui/app/i18n/de.ts +0 -432
  306. package/layers/ui/app/i18n/en.ts +0 -434
  307. package/layers/ui/app/utils/boolean-field.ts +0 -10
  308. package/layers/ui/app/utils/field-component.ts +0 -22
  309. package/layers/ui/app/utils/field-empty.ts +0 -21
  310. package/layers/ui/app/utils/field-registry.ts +0 -36
  311. package/layers/ui/app/utils/humanize.ts +0 -14
  312. package/layers/ui/app/utils/icons.ts +0 -143
  313. package/layers/ui/app/utils/iso-date.ts +0 -18
  314. package/layers/ui/app/utils/localized.ts +0 -13
  315. package/layers/ui/app/utils/record-context.ts +0 -15
  316. package/layers/ui/app/utils/reorder.ts +0 -8
  317. package/layers/ui/app/utils/richtext-preserve-class.ts +0 -31
  318. package/layers/ui/app/utils/useFieldA11y.ts +0 -40
  319. package/layers/ui/nuxt.config.ts +0 -9
  320. package/nuxt.config.ts +0 -108
  321. package/scripts/consumer-template-ci.mjs +0 -160
  322. package/scripts/copy-create-payload.mjs +0 -49
  323. package/scripts/coverage-all.mjs +0 -163
  324. package/scripts/dashboard.mjs +0 -89
  325. package/scripts/hash-password.mjs +0 -29
  326. package/scripts/kestrel.mjs +0 -240
  327. package/scripts/lib/cli.mjs +0 -129
  328. package/scripts/lib/coverage-model.mjs +0 -51
  329. package/scripts/lib/password.mjs +0 -19
  330. package/scripts/lib/scaffold.mjs +0 -205
  331. package/scripts/port-graph.mjs +0 -350
  332. package/scripts/typecheck.mjs +0 -62
  333. package/templates/starter/README.md +0 -65
  334. package/templates/starter/_env.example +0 -17
  335. package/templates/starter/_gitignore +0 -26
  336. package/templates/starter/_package.json +0 -22
  337. package/templates/starter/app/app.vue +0 -7
  338. package/templates/starter/app/blocks/Prose.vue +0 -13
  339. package/templates/starter/app/layouts/default.vue +0 -6
  340. package/templates/starter/nuxt.config.ts +0 -23
  341. package/templates/starter/pnpm-workspace.yaml +0 -8
  342. package/templates/starter/tsconfig.json +0 -3
@@ -1,67 +0,0 @@
1
- <script setup lang="ts">
2
- import { computed } from 'vue'
3
- import type { LayoutNode, SerializedBlock } from '@michaelthielemann/kestrel-core'
4
- import type { BlockRow } from '../utils/block-tree'
5
- import { resolveLocalized } from '../../../ui/app/utils/localized'
6
-
7
- // The fields pane for the currently selected block: delegates to the shared <KestrelFieldLayout> renderer,
8
- // driven by the selected block's props. `def` is the selected block's resolved definition (the parent
9
- // already has it). Errors are the id-keyed BlockErrorMap entry for this block (depth-independent); edits
10
- // emit (key, value) for an id-addressed setProp on the shared tree.
11
- const props = defineProps<{
12
- block: BlockRow
13
- def?: SerializedBlock
14
- locale: string
15
- errors?: Record<string, string>
16
- /** Field keys in this block that hold a stale reference — shown as a non-blocking warning note. */
17
- deadFields?: Set<string>
18
- disabled?: boolean
19
- }>()
20
- const emit = defineEmits<{ update: [key: string, value: unknown] }>()
21
-
22
- const { t, lang } = useT()
23
- const subFields = computed(() =>
24
- Object.fromEntries(Object.entries(props.def?.fields ?? {}).map(([k, v]) => [k, asFieldDef(v)])),
25
- )
26
- // Block-level `fieldLayout` authoring is deferred (v1 ships collections + repeaters); the render path is
27
- // already layout-ready via this cast, so a future serializeBlock that carries a layout flows through with
28
- // no further change. One documented cast, same category as `asFieldDef`.
29
- const blockLayout = computed(() => (props.def as { fieldLayout?: LayoutNode[] } | undefined)?.fieldLayout)
30
- </script>
31
-
32
- <template>
33
- <div class="block-fields">
34
- <p class="block-fields__title">{{ resolveLocalized(def?.label, lang) ?? block.type }}</p>
35
- <KestrelFieldLayout
36
- :layout="blockLayout"
37
- :fields="subFields"
38
- :values="block.props"
39
- :errors="errors"
40
- :dead-fields="deadFields"
41
- :locale="locale"
42
- :disabled="disabled"
43
- @update="(key, value) => emit('update', key, value)"
44
- />
45
- <p v-if="!Object.keys(subFields).length" class="block-fields__empty">{{ t('blocks.noFields') }}</p>
46
- </div>
47
- </template>
48
-
49
- <style lang="scss">
50
- .block-fields {
51
- display: flex;
52
- flex-direction: column;
53
- gap: var(--space-3);
54
-
55
- &__title {
56
- margin: 0;
57
- font-size: var(--text-sm);
58
- font-weight: var(--weight-medium);
59
- color: var(--color-text-muted);
60
- text-transform: capitalize;
61
- }
62
- &__empty {
63
- font-size: var(--text-sm);
64
- color: var(--color-text-muted);
65
- }
66
- }
67
- </style>
@@ -1,413 +0,0 @@
1
- <script setup lang="ts">
2
- import { computed, inject, onMounted, onUnmounted, ref, watch } from 'vue'
3
- import type { SerializedBlock } from '@michaelthielemann/kestrel-core'
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
- // The layout wraps the frame's own document (`<NuxtLayout>`, a parent the postMessage bridge can't reach
26
- // from inside its slot), so a layout change can't ride the content push — only a fresh navigation
27
- // re-resolves it. Forcing one needs an actual URL change (a `src` re-assignment to the same string is a
28
- // no-op), so a change bumps this counter, which `frameSrc` folds into the URL as a harmless query param.
29
- const layoutReloadKey = ref(0)
30
- const frameSrc = computed(() => {
31
- if (!layoutReloadKey.value) return src.value
32
- return `${src.value}${src.value.includes('?') ? '&' : '?'}_r=${layoutReloadKey.value}`
33
- })
34
-
35
- // ---- populate (pipeline: resolve media ids + internal links into reactive caches) ----
36
- // Block field schemas (non-blocking: byType fills once loaded, then the next send carries the media).
37
- const { blocks: blockDefs, load } = useBlocks()
38
- load()
39
- const byType = computed<Record<string, SerializedBlock>>(() =>
40
- Object.fromEntries((blockDefs.value ?? []).map((b) => [b.name, b])),
41
- )
42
- const items = computed(() => (Array.isArray(props.content) ? props.content : []))
43
- // `locale` is a getter so a locale change re-resolves media (its cache is per-locale).
44
- const { ensure: ensureMedia, resolve: resolveMedia } = useMediaResolver(() => props.locale ?? 'en')
45
- const { ensure: ensureLinks, resolve: resolveLink } = useLinkResolver()
46
- // The current resolve round. The ready-handshake push awaits it: the frame's first paint is the
47
- // SERVER-populated page, so pushing a not-yet-resolved client tree would momentarily strip its images/
48
- // hrefs. With warm (or empty) caches the promise settles immediately — no perceptible delay.
49
- let ensureRound: Promise<unknown> = Promise.resolve()
50
- watch(
51
- [items, byType, () => props.locale],
52
- ([list, types]) => {
53
- ensureRound = Promise.allSettled([
54
- ensureMedia(collectMediaIds(list, types)),
55
- ensureLinks(collectLinkRefs(list, types)),
56
- ])
57
- },
58
- { immediate: true, deep: true },
59
- )
60
- const populated = computed(() =>
61
- populateBlocksLinks(populateBlocksMedia(items.value, byType.value, resolveMedia), byType.value, resolveLink),
62
- )
63
-
64
- // ---- channel (parent side) ----
65
- const frame = ref<HTMLIFrameElement | null>(null)
66
- // True once the CURRENT frame document said hello; sends before that are pointless (lost on load).
67
- const ready = ref(false)
68
- let sender: PreviewSender | null = null
69
-
70
- function pushContent() {
71
- sender?.sendContent(populated.value as PreviewBlockNode[])
72
- }
73
-
74
- function onMessage(e: MessageEvent) {
75
- if (!acceptsFrameEvent(e, frame.value?.contentWindow ?? null, window.location.origin)) return
76
- const m = parseFrameMessage(e.data)
77
- if (!m) return
78
- if (m.kestrel === 'preview:ready') {
79
- // Fresh document (first load OR reload/src change) — send it the full current state, once the
80
- // in-flight media/link resolve settles (see ensureRound); `ready` may flip meanwhile (reload).
81
- ready.value = true
82
- void ensureRound.then(() => { if (ready.value) pushContent() })
83
- } else {
84
- emit('select', m.id)
85
- }
86
- }
87
-
88
- // Every load starts a document that must handshake before it may receive pushes. If consumer script
89
- // navigated the canvas away (programmatic router push — clicks/submits are already intercepted in the
90
- // bridge), the loaded document is not the preview page: snap the frame back to the preview URL.
91
- function onFrameLoad() {
92
- ready.value = false
93
- const el = frame.value
94
- if (!el) return
95
- try {
96
- const loc = el.contentWindow?.location
97
- if (loc && loc.pathname + loc.search !== frameSrc.value) el.src = frameSrc.value
98
- } catch {
99
- el.src = frameSrc.value // cross-origin navigation — location unreadable, snap back
100
- }
101
- }
102
-
103
- // Manual fallback for any residual stale component state: reload the iframe. This remounts EVERY
104
- // component in the frame (not just, say, a stale srcset) and re-handshakes — the fresh document posts
105
- // `preview:ready`, and onMessage re-pushes the full populated tree after ensureRound. `ready=false`
106
- // guards pushes to the doc that's tearing down.
107
- function refresh() {
108
- ready.value = false
109
- const el = frame.value
110
- if (!el) return
111
- try {
112
- el.contentWindow?.location.reload() // same URL → onFrameLoad's snap-back check passes
113
- } catch {
114
- el.src = frameSrc.value // cross-origin: location unreadable → renavigate
115
- }
116
- }
117
-
118
- onMounted(() => {
119
- sender = createPreviewSender(() => frame.value?.contentWindow ?? null, window.location.origin, () => props.selectedId ?? null)
120
- window.addEventListener('message', onMessage)
121
- })
122
- onUnmounted(() => {
123
- sender?.dispose()
124
- sender = null
125
- window.removeEventListener('message', onMessage)
126
- })
127
-
128
- // Keystroke-live: every populated-tree change (edit, media/link cache fill) schedules a send; the
129
- // channel coalesces bursts to one message per animation frame. Selection posts immediately (tiny).
130
- watch(populated, () => { if (ready.value) pushContent() })
131
- watch(() => props.selectedId, (id) => { if (ready.value) sender?.sendSelected(id ?? null) })
132
- // A src change reloads the iframe (locale switch, first save assigning a URL) → wait for its new ready.
133
- watch(src, () => { ready.value = false })
134
- // See `layoutReloadKey` above: a layout change forces a fresh navigation, the only way to re-resolve it.
135
- watch(() => ctx?.values?.layout, () => { layoutReloadKey.value++; ready.value = false })
136
-
137
- // ---- responsive preview: device presets (quick-fill) + automatic scale-to-fit + custom W×H ----
138
- // The iframe renders at the target resolution's REAL px (so the page fires its true breakpoints); a
139
- // uniform transform only shrinks it visually to fit the pane. Desktop keeps the config-driven breakpoint
140
- // width but an 'auto' height that fills the pane vertically (the iframe's own document scrolls).
141
- const previewDesktopWidth = Number((useRuntimeConfig().public as { previewDesktopWidth?: number }).previewDesktopWidth) || 1440
142
- const presets = PRESETS(previewDesktopWidth)
143
- const { width, height } = usePreviewViewport(previewDesktopWidth)
144
- const activePreset = computed(() => matchPreset(width.value, height.value, presets))
145
-
146
- function selectPreset(p: ViewportPreset) {
147
- width.value = p.w
148
- height.value = p.h
149
- }
150
- // Commit a W×H input on change (blur/Enter — `change` falls through to the primitive's root <input>), not
151
- // per keystroke, so the frame doesn't thrash mid-typing. Empty → keep the current value; else full clamp.
152
- function onDimCommit(target: 'w' | 'h', e: Event) {
153
- const el = e.target as HTMLInputElement
154
- if (el.value === '') {
155
- // A number input also reports '' while it holds un-parsable editing text ('-', '1e'); that must keep
156
- // the current value, not silently switch the axis. A genuinely EMPTIED field means "auto": the axis
157
- // fills the pane. (Clearing height under the fixed desktop width re-selects the Desktop preset.)
158
- if (el.validity?.badInput) return
159
- if (target === 'w') width.value = 'auto'
160
- else height.value = 'auto'
161
- return
162
- }
163
- const v = clampDim(Number(el.value), DIM_MIN, target === 'w' ? WIDTH_MAX : HEIGHT_MAX)
164
- if (v === null) return
165
- if (target === 'w') width.value = v
166
- else height.value = v
167
- }
168
-
169
- // Both stage axes drive the fit scale — measured with a ResizeObserver (VueUse isn't wired; a manual
170
- // observer is the in-repo idiom, cf. useTheme's matchMedia). `scale` stays 1 until the first measure.
171
- const stage = ref<HTMLElement | null>(null)
172
- const availW = ref(0)
173
- const availH = ref(0)
174
- let ro: ResizeObserver | null = null
175
- const scale = computed(() => fitScale(availW.value, availH.value, width.value, height.value))
176
- const scalePct = computed(() => Math.round(scale.value * 100))
177
- // Resolve each Dim to real px: fixed passes through, an 'auto' axis fills the pane at the current scale
178
- // (falls back to the config width / 900 before measurement so SSR/tests still have a sized frame).
179
- const frameW = computed(() => resolveDim(availW.value, width.value, scale.value, previewDesktopWidth))
180
- const frameH = computed(() => resolveDim(availH.value, height.value, scale.value, 900))
181
-
182
- // The iframe keeps its real target px and is scaled from its top-left; the holder occupies the SCALED
183
- // footprint so centering behaves. The stage clips (overflow:hidden) so only the iframe document scrolls.
184
- const frameStyle = computed(() => ({
185
- width: `${frameW.value}px`,
186
- height: `${frameH.value}px`,
187
- transform: `scale(${scale.value})`,
188
- transformOrigin: 'top left',
189
- }))
190
- const viewportStyle = computed(() => ({
191
- width: `${frameW.value * scale.value}px`,
192
- height: `${frameH.value * scale.value}px`,
193
- }))
194
-
195
- // Content box (padding excluded) so the initial seed matches the ResizeObserver's `contentRect` — both
196
- // must agree, else the frame footprint would briefly exceed the padded stage (clipped, not scrolled).
197
- function contentBox(el: HTMLElement) {
198
- const cs = getComputedStyle(el)
199
- return {
200
- width: el.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight),
201
- height: el.clientHeight - parseFloat(cs.paddingTop) - parseFloat(cs.paddingBottom),
202
- }
203
- }
204
- onMounted(() => {
205
- const el = stage.value
206
- if (!el || typeof ResizeObserver === 'undefined') return
207
- ro = new ResizeObserver((entries) => {
208
- const b = entries[0]?.contentRect ?? contentBox(el)
209
- availW.value = b.width
210
- availH.value = b.height
211
- })
212
- ro.observe(el)
213
- const b = contentBox(el)
214
- availW.value = b.width
215
- availH.value = b.height
216
- })
217
- onUnmounted(() => { ro?.disconnect(); ro = null })
218
- </script>
219
-
220
- <template>
221
- <section class="block-preview" :aria-label="t('preview.ariaLabel')">
222
- <div class="block-preview__bar">
223
- <p class="block-preview__label">{{ t('preview.label') }}</p>
224
-
225
- <div class="block-preview__tools">
226
- <div class="block-preview__group">
227
- <KestrelUiTooltip>
228
- <KestrelUiButton
229
- type="button"
230
- variant="ghost"
231
- size="sm"
232
- icon="rotate-cw"
233
- :aria-label="t('preview.refresh')"
234
- @click="refresh"
235
- />
236
- <template #content>{{ t('preview.refresh') }}</template>
237
- </KestrelUiTooltip>
238
- </div>
239
-
240
- <span class="block-preview__sep" aria-hidden="true" />
241
-
242
- <div class="block-preview__group" role="group" :aria-label="t('preview.deviceLabel')">
243
- <KestrelUiTooltip v-for="p in presets" :key="p.key">
244
- <KestrelUiButton
245
- type="button"
246
- variant="ghost"
247
- size="sm"
248
- :icon="p.icon"
249
- :aria-label="t(p.label)"
250
- :aria-pressed="activePreset === p.key"
251
- :class="{ 'block-preview__seg--active': activePreset === p.key }"
252
- @click="selectPreset(p)"
253
- />
254
- <template #content>{{ t(p.label) }} · {{ p.w }} × {{ p.h === 'auto' ? t('preview.auto') : p.h }}</template>
255
- </KestrelUiTooltip>
256
- </div>
257
-
258
- <span class="block-preview__sep" aria-hidden="true" />
259
-
260
- <!-- Read-only: scale is always automatic (two-axis fit), so the % is an indicator, not a control. -->
261
- <span class="block-preview__pct" aria-hidden="true">{{ scalePct }}%</span>
262
-
263
- <span class="block-preview__sep" aria-hidden="true" />
264
-
265
- <div class="block-preview__dims">
266
- <!-- Either axis can be 'Auto' (fills the pane): typing a number fixes it, clearing the field
267
- returns it to Auto. Height is Auto under the Desktop preset. -->
268
- <KestrelUiNumberInput
269
- slim
270
- class="block-preview__dim"
271
- :model-value="width === 'auto' ? null : width"
272
- :min="DIM_MIN"
273
- :max="WIDTH_MAX"
274
- :placeholder="width === 'auto' ? t('preview.auto') : undefined"
275
- :aria-label="t('preview.width')"
276
- @change="onDimCommit('w', $event)"
277
- />
278
- <span class="block-preview__x" aria-hidden="true">×</span>
279
- <KestrelUiNumberInput
280
- slim
281
- class="block-preview__dim"
282
- :model-value="height === 'auto' ? null : height"
283
- :min="DIM_MIN"
284
- :max="HEIGHT_MAX"
285
- :placeholder="height === 'auto' ? t('preview.auto') : undefined"
286
- :aria-label="t('preview.height')"
287
- @change="onDimCommit('h', $event)"
288
- />
289
- <span class="block-preview__unit" aria-hidden="true">px</span>
290
- </div>
291
- </div>
292
- </div>
293
-
294
- <div ref="stage" class="block-preview__stage">
295
- <div class="block-preview__viewport" :style="viewportStyle">
296
- <iframe ref="frame" class="block-preview__frame" :src="frameSrc" :style="frameStyle" :title="t('preview.label')" @load="onFrameLoad" />
297
- </div>
298
- </div>
299
- </section>
300
- </template>
301
-
302
- <style lang="scss">
303
- // The iframe owns rendering + scrolling; this host only frames it and hosts the device toggle.
304
- .block-preview {
305
- flex: 1 1 auto;
306
- min-height: 0;
307
- display: flex;
308
- flex-direction: column;
309
- gap: var(--space-2);
310
-
311
- &__bar {
312
- display: flex;
313
- flex-wrap: wrap;
314
- align-items: center;
315
- justify-content: space-between;
316
- gap: var(--space-2);
317
- flex: 0 0 auto;
318
- }
319
- // Pane heading, folded onto the toolbar row so the preview header is a single thin line (the outer
320
- // editor pane's own "Preview" label is hidden below to avoid a duplicate).
321
- &__label {
322
- margin: 0;
323
- font-size: var(--text-sm);
324
- font-weight: var(--weight-medium);
325
- color: var(--color-text-muted);
326
- flex: 0 0 auto;
327
- }
328
- // Controls cluster to the right of the heading, wrapping among themselves; the whole cluster only
329
- // wraps below the heading when the middle pane is too narrow to fit both.
330
- &__tools {
331
- display: flex;
332
- flex-wrap: wrap;
333
- align-items: center;
334
- justify-content: flex-end;
335
- gap: var(--space-2);
336
- flex: 1 1 auto;
337
- min-width: 0;
338
- }
339
- &__group {
340
- display: flex;
341
- align-items: center;
342
- gap: var(--space-1);
343
- }
344
- // Active segment: neutral fill + indigo glyph (matches the button-group active idiom).
345
- &__seg--active {
346
- color: var(--color-primary);
347
- background: var(--color-surface-2);
348
- }
349
- &__sep {
350
- align-self: stretch;
351
- width: 1px;
352
- min-height: 1.25rem;
353
- background: var(--color-border);
354
- }
355
- &__pct {
356
- min-width: 3.5ch;
357
- text-align: right;
358
- font-size: var(--text-sm);
359
- color: var(--color-text-muted);
360
- font-variant-numeric: tabular-nums;
361
- }
362
- &__dims {
363
- display: flex;
364
- align-items: center;
365
- gap: var(--space-1);
366
- font-size: var(--text-sm);
367
- color: var(--color-text-muted);
368
- }
369
- // Compact numeric inputs (the primitive is width:100% by default — override under our wrapper so the
370
- // selector wins by specificity without touching the shared component). 6rem fits up to 5 typed digits
371
- // without clipping; flex:none stops the narrow middle pane from shrinking them back below that width.
372
- &__dims .ui-number {
373
- width: 6rem;
374
- flex: none;
375
- font-variant-numeric: tabular-nums;
376
- }
377
-
378
- &__stage {
379
- flex: 1 1 auto;
380
- min-height: 0;
381
- display: flex;
382
- justify-content: center;
383
- align-items: center;
384
- // The container NEVER scrolls: the frame is always scaled to fit both axes, and any over-height
385
- // content scrolls inside the iframe's own document. Center the (possibly letterboxed) device frame.
386
- overflow: hidden;
387
- // No padding: the pane already has its own, and an inset here would misalign the frame's top corners
388
- // with the rest of the editor's content edge.
389
-
390
- // Stacked single-column editor (matches the editor3 breakpoint in BlocksBody): the panes become
391
- // content-sized there, so give the stage a concrete height to measure + scale the frame within.
392
- @media (max-width: 1100px) {
393
- height: 70vh;
394
- }
395
- }
396
- // Occupies the SCALED footprint of the frame so centering + overflow behave; owns the crisp 1px border
397
- // (box-shadow, unscaled). Deliberately square: the preview must read as the RAW page, not a device mock,
398
- // so no corner radius rounds off the rendered edges. `overflow: hidden` still guards subpixel spill.
399
- &__viewport {
400
- flex: 0 0 auto;
401
- box-shadow: 0 0 0 1px var(--color-border);
402
- background: #fff;
403
- overflow: hidden;
404
- transition: width 160ms ease, height 160ms ease;
405
- }
406
- &__frame {
407
- display: block;
408
- border: 0;
409
- background: #fff;
410
- transition: transform 160ms ease;
411
- }
412
- }
413
- </style>