@michaelthielemann/kestrel 4.1.3 → 5.0.0

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 (340) hide show
  1. package/README.md +26 -201
  2. package/dist/boot.d.ts +45 -0
  3. package/dist/boot.js +274 -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 +157 -0
  15. package/dist/defineConfig.js +41 -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/cron.d.ts +10 -0
  43. package/dist/triggers/cron.js +44 -0
  44. package/dist/triggers/cronMatch.d.ts +10 -0
  45. package/dist/triggers/cronMatch.js +68 -0
  46. package/dist/triggers/http.d.ts +58 -0
  47. package/dist/triggers/http.js +292 -0
  48. package/package.json +25 -151
  49. package/NOTICE +0 -12
  50. package/kestrel.config.ts +0 -28
  51. package/layers/access/nuxt.config.ts +0 -5
  52. package/layers/access/server/middleware/00.ip-allowlist.ts +0 -52
  53. package/layers/access/server/middleware/access-guard.ts +0 -32
  54. package/layers/access/server/types.d.ts +0 -7
  55. package/layers/admin/app/components/AdminAccount.vue +0 -212
  56. package/layers/admin/app/components/AdminNav.vue +0 -44
  57. package/layers/admin/app/components/BlockFields.vue +0 -67
  58. package/layers/admin/app/components/BlockPreview.vue +0 -413
  59. package/layers/admin/app/components/BlockTree.vue +0 -433
  60. package/layers/admin/app/components/BlocksBody.vue +0 -163
  61. package/layers/admin/app/components/CollectionDeleteDialog.vue +0 -38
  62. package/layers/admin/app/components/CollectionEditor.vue +0 -324
  63. package/layers/admin/app/components/CollectionList.vue +0 -429
  64. package/layers/admin/app/components/CollectionListBulkBar.vue +0 -62
  65. package/layers/admin/app/components/CollectionListColumnsPanel.vue +0 -37
  66. package/layers/admin/app/components/CollectionListFilterPanel.vue +0 -145
  67. package/layers/admin/app/components/CollectionListPager.vue +0 -70
  68. package/layers/admin/app/components/CollectionListTable.vue +0 -347
  69. package/layers/admin/app/components/EditorStatus.vue +0 -211
  70. package/layers/admin/app/components/EditorUnsupported.vue +0 -28
  71. package/layers/admin/app/components/FieldsBody.vue +0 -23
  72. package/layers/admin/app/components/LocaleBar.vue +0 -107
  73. package/layers/admin/app/components/PageFields.vue +0 -129
  74. package/layers/admin/app/components/PageFieldsPane.vue +0 -39
  75. package/layers/admin/app/components/SeoFields.vue +0 -210
  76. package/layers/admin/app/components/SingletonEditor.vue +0 -93
  77. package/layers/admin/app/composables/useAuth.ts +0 -68
  78. package/layers/admin/app/composables/useBlockTree.ts +0 -95
  79. package/layers/admin/app/composables/useBlocks.ts +0 -7
  80. package/layers/admin/app/composables/useCachedResource.ts +0 -35
  81. package/layers/admin/app/composables/useCollectionOps.ts +0 -102
  82. package/layers/admin/app/composables/useCollections.ts +0 -6
  83. package/layers/admin/app/composables/useContentLocales.ts +0 -23
  84. package/layers/admin/app/composables/useEditForm.ts +0 -402
  85. package/layers/admin/app/composables/useLinkResolver.ts +0 -40
  86. package/layers/admin/app/composables/useListBatchActions.ts +0 -78
  87. package/layers/admin/app/composables/useListColumns.ts +0 -53
  88. package/layers/admin/app/composables/useListFilterDraft.ts +0 -129
  89. package/layers/admin/app/composables/useListRows.ts +0 -75
  90. package/layers/admin/app/composables/useListSelection.ts +0 -28
  91. package/layers/admin/app/composables/useListUrlState.ts +0 -53
  92. package/layers/admin/app/composables/usePendingPublishPoll.ts +0 -15
  93. package/layers/admin/app/composables/usePreviewViewport.ts +0 -30
  94. package/layers/admin/app/composables/usePublishStatus.ts +0 -107
  95. package/layers/admin/app/composables/useRailCollapsed.ts +0 -14
  96. package/layers/admin/app/composables/useTheme.ts +0 -36
  97. package/layers/admin/app/composables/useToolbarPanel.ts +0 -38
  98. package/layers/admin/app/composables/useUnsavedGuard.ts +0 -22
  99. package/layers/admin/app/layouts/admin.vue +0 -266
  100. package/layers/admin/app/middleware/admin-auth.ts +0 -4
  101. package/layers/admin/app/pages/admin/[collection]/[id].vue +0 -203
  102. package/layers/admin/app/pages/admin/[collection]/index.vue +0 -62
  103. package/layers/admin/app/pages/admin/index.vue +0 -118
  104. package/layers/admin/app/pages/admin/login.vue +0 -68
  105. package/layers/admin/app/pages/admin/media.vue +0 -28
  106. package/layers/admin/app/pages/admin/references.vue +0 -146
  107. package/layers/admin/app/plugins/admin-style-guard.client.ts +0 -76
  108. package/layers/admin/app/plugins/reauth.client.ts +0 -20
  109. package/layers/admin/app/utils/admin-style-guard.ts +0 -20
  110. package/layers/admin/app/utils/block-tree.ts +0 -214
  111. package/layers/admin/app/utils/collection-ops.ts +0 -69
  112. package/layers/admin/app/utils/dead-refs.ts +0 -30
  113. package/layers/admin/app/utils/edit-form.ts +0 -267
  114. package/layers/admin/app/utils/editor-expose.ts +0 -36
  115. package/layers/admin/app/utils/editor-form-context.ts +0 -82
  116. package/layers/admin/app/utils/editor-registry.ts +0 -20
  117. package/layers/admin/app/utils/field-keys.ts +0 -11
  118. package/layers/admin/app/utils/list-cell.ts +0 -46
  119. package/layers/admin/app/utils/list-columns.ts +0 -142
  120. package/layers/admin/app/utils/list-query.ts +0 -96
  121. package/layers/admin/app/utils/nav-active.ts +0 -9
  122. package/layers/admin/app/utils/populate-blocks.ts +0 -288
  123. package/layers/admin/app/utils/preview-channel.ts +0 -62
  124. package/layers/admin/app/utils/preview-viewport.ts +0 -80
  125. package/layers/admin/app/utils/reauth.ts +0 -67
  126. package/layers/admin/app/utils/record-title.ts +0 -15
  127. package/layers/admin/app/utils/register-builtin-editors.ts +0 -9
  128. package/layers/admin/app/utils/safe-redirect.ts +0 -12
  129. package/layers/admin/nuxt.config.ts +0 -20
  130. package/layers/auth/nuxt.config.ts +0 -1
  131. package/layers/auth/server/plugins/01.register-auth-pipelines.ts +0 -7
  132. package/layers/collections/nuxt.config.ts +0 -1
  133. package/layers/collections/server/plugins/01.register.ts +0 -29
  134. package/layers/collections/server/plugins/02.register-relation-populate.ts +0 -18
  135. package/layers/core/app/composables/layouts.ts +0 -5
  136. package/layers/core/app/utils/field-factories.ts +0 -129
  137. package/layers/core/app/utils/layouts.ts +0 -38
  138. package/layers/core/modules/auto-discovery/extract-block.ts +0 -179
  139. package/layers/core/modules/auto-discovery/index.ts +0 -112
  140. package/layers/core/modules/auto-discovery/package-registry.ts +0 -19
  141. package/layers/core/modules/auto-discovery/scan.ts +0 -170
  142. package/layers/core/modules/auto-discovery/virtual.d.ts +0 -20
  143. package/layers/core/modules/component-namespace/index.ts +0 -44
  144. package/layers/core/modules/component-namespace/shared.ts +0 -24
  145. package/layers/core/modules/kestrel/app-shell.ts +0 -55
  146. package/layers/core/modules/kestrel/auto-imports.ts +0 -44
  147. package/layers/core/modules/kestrel/index.ts +0 -144
  148. package/layers/core/modules/plugin-order/index.ts +0 -69
  149. package/layers/core/modules/plugin-order/plugin-order.ts +0 -273
  150. package/layers/core/nuxt.config.ts +0 -10
  151. package/layers/core/server/api/[...path].ts +0 -98
  152. package/layers/core/server/plugins/00.config.ts +0 -12
  153. package/layers/core/server/plugins/00.migrate.ts +0 -12
  154. package/layers/core/server/plugins/01.register-introspection-pipeline.ts +0 -5
  155. package/layers/core/server/plugins/01.register-openapi-pipeline.ts +0 -5
  156. package/layers/core/server/plugins/01.register-tooling-pipelines.ts +0 -5
  157. package/layers/core/server/plugins/02.schema-sync.ts +0 -58
  158. package/layers/core/server/plugins/04.outbox-worker.ts +0 -31
  159. package/layers/core/server/plugins/05.reindex-refs.ts +0 -5
  160. package/layers/core/server/routes/__kestrel/dashboard.get.ts +0 -29
  161. package/layers/core/server/tasks/db/migrate-module.ts +0 -83
  162. package/layers/core/server/tasks/db/migrate-revisions.ts +0 -20
  163. package/layers/core/server/tasks/db/migrate.ts +0 -50
  164. package/layers/core/server/utils/server-config.ts +0 -81
  165. package/layers/fields/nuxt.config.ts +0 -1
  166. package/layers/fields/server/plugins/01.register-blocks-pipeline.ts +0 -7
  167. package/layers/fields/server/plugins/01.register-field-populate.ts +0 -11
  168. package/layers/media/app/components/KestrelImg.vue +0 -94
  169. package/layers/media/app/components/MediaConflictDialog.vue +0 -35
  170. package/layers/media/app/components/MediaDeleteDialog.vue +0 -52
  171. package/layers/media/app/components/MediaGrid.vue +0 -113
  172. package/layers/media/app/components/MediaImage.vue +0 -70
  173. package/layers/media/app/components/MediaLibrary.vue +0 -456
  174. package/layers/media/app/components/MediaNewFolderDialog.vue +0 -32
  175. package/layers/media/app/components/MediaPathBar.vue +0 -163
  176. package/layers/media/app/components/MediaPicker.vue +0 -19
  177. package/layers/media/app/components/MediaRenameDialog.vue +0 -32
  178. package/layers/media/app/components/MediaTable.vue +0 -75
  179. package/layers/media/app/components/MediaThumb.vue +0 -25
  180. package/layers/media/app/components/MediaToolbar.vue +0 -90
  181. package/layers/media/app/components/MediaUploadDialog.vue +0 -87
  182. package/layers/media/app/components/MediaViewer.vue +0 -137
  183. package/layers/media/app/components/field/Media.vue +0 -263
  184. package/layers/media/app/composables/useBlurUp.ts +0 -55
  185. package/layers/media/app/composables/useMediaClipboard.ts +0 -14
  186. package/layers/media/app/composables/useMediaContextMenu.ts +0 -56
  187. package/layers/media/app/composables/useMediaDnd.ts +0 -56
  188. package/layers/media/app/composables/useMediaLibrary.ts +0 -152
  189. package/layers/media/app/composables/useMediaOps.ts +0 -50
  190. package/layers/media/app/composables/useMediaRelocate.ts +0 -39
  191. package/layers/media/app/composables/useMediaResolver.ts +0 -50
  192. package/layers/media/app/composables/useMediaUpload.ts +0 -93
  193. package/layers/media/app/composables/useMediaVariant.ts +0 -34
  194. package/layers/media/app/plugins/field-media.client.ts +0 -6
  195. package/layers/media/app/utils/ai-disclosure.ts +0 -19
  196. package/layers/media/app/utils/dnd.ts +0 -69
  197. package/layers/media/app/utils/library.ts +0 -81
  198. package/layers/media/app/utils/ops.ts +0 -60
  199. package/layers/media/app/utils/picture-model.ts +0 -125
  200. package/layers/media/app/utils/thumbhash.ts +0 -19
  201. package/layers/media/app/utils/variant-name.ts +0 -20
  202. package/layers/media/nuxt.config.ts +0 -7
  203. package/layers/media/server/middleware/ondemand-variants.ts +0 -38
  204. package/layers/media/server/plugins/01.register-media-pipelines.ts +0 -7
  205. package/layers/media/server/plugins/02.register-media.ts +0 -22
  206. package/layers/media/server/plugins/04.variant-capture.ts +0 -27
  207. package/layers/media/server/plugins/05.media-cleanup.ts +0 -5
  208. package/layers/media/server/tasks/media/backfill.ts +0 -17
  209. package/layers/public/app/app.vue +0 -5
  210. package/layers/public/app/components/BlockRenderer.vue +0 -56
  211. package/layers/public/app/components/KestrelLink.vue +0 -26
  212. package/layers/public/app/components/KestrelPreviewBridge.vue +0 -133
  213. package/layers/public/app/composables/public-page.ts +0 -24
  214. package/layers/public/app/error.vue +0 -12
  215. package/layers/public/app/layouts/default.vue +0 -5
  216. package/layers/public/app/pages/[...slug].vue +0 -239
  217. package/layers/public/app/pages/__kestrel/preview.vue +0 -46
  218. package/layers/public/app/utils/block-edit-context.ts +0 -16
  219. package/layers/public/app/utils/json-ld.ts +0 -139
  220. package/layers/public/app/utils/link-href.ts +0 -46
  221. package/layers/public/app/utils/page-head.ts +0 -84
  222. package/layers/public/app/utils/page-layout.ts +0 -18
  223. package/layers/public/app/utils/preview-protocol.ts +0 -117
  224. package/layers/public/app/utils/public-route.ts +0 -46
  225. package/layers/public/app/utils/site-head.ts +0 -43
  226. package/layers/public/modules/deploy-output/index.ts +0 -80
  227. package/layers/public/modules/prerender-routes/discover.ts +0 -37
  228. package/layers/public/modules/prerender-routes/index.ts +0 -88
  229. package/layers/public/modules/prerender-routes/routes.ts +0 -16
  230. package/layers/public/modules/prune-media/index.ts +0 -48
  231. package/layers/public/modules/prune-media/prune-media.ts +0 -81
  232. package/layers/public/nuxt.config.ts +0 -14
  233. package/layers/public/server/delivery-live/serve.ts +0 -111
  234. package/layers/public/server/middleware/delivery-live-catchall.ts +0 -7
  235. package/layers/public/server/pipelines/index.ts +0 -23
  236. package/layers/public/server/plugins/00.ensure-snapshot-triggers.ts +0 -14
  237. package/layers/public/server/plugins/01.register-public-pipelines.ts +0 -7
  238. package/layers/public/server/plugins/02.register-links.ts +0 -16
  239. package/layers/public/server/plugins/03.redirects.ts +0 -55
  240. package/layers/public/server/plugins/05.plan-publish.ts +0 -5
  241. package/layers/public/server/plugins/zz.publish.ts +0 -127
  242. package/layers/public/server/routes/llms-full.txt.get.ts +0 -99
  243. package/layers/public/server/routes/llms.txt.get.ts +0 -66
  244. package/layers/public/server/routes/redirects.json.get.ts +0 -58
  245. package/layers/public/server/routes/robots.txt.get.ts +0 -11
  246. package/layers/public/server/routes/sitemap.xml.get.ts +0 -79
  247. package/layers/public/server/tasks/publish/run.ts +0 -32
  248. package/layers/public/server/utils/publish/render-live.ts +0 -26
  249. package/layers/ui/app/assets/scss/_base.scss +0 -16
  250. package/layers/ui/app/assets/scss/_datepicker.scss +0 -82
  251. package/layers/ui/app/assets/scss/_mixins.scss +0 -50
  252. package/layers/ui/app/assets/scss/_reset.scss +0 -65
  253. package/layers/ui/app/assets/scss/_tokens.scss +0 -183
  254. package/layers/ui/app/assets/scss/main.scss +0 -4
  255. package/layers/ui/app/components/field/Boolean.vue +0 -30
  256. package/layers/ui/app/components/field/Choice.vue +0 -73
  257. package/layers/ui/app/components/field/Datetime.vue +0 -98
  258. package/layers/ui/app/components/field/DeadRefNote.vue +0 -28
  259. package/layers/ui/app/components/field/Json.vue +0 -62
  260. package/layers/ui/app/components/field/Layout.vue +0 -138
  261. package/layers/ui/app/components/field/Link.vue +0 -112
  262. package/layers/ui/app/components/field/LinkInternalPicker.vue +0 -95
  263. package/layers/ui/app/components/field/Number.vue +0 -30
  264. package/layers/ui/app/components/field/Relation.vue +0 -49
  265. package/layers/ui/app/components/field/Renderer.vue +0 -30
  266. package/layers/ui/app/components/field/Repeater.vue +0 -460
  267. package/layers/ui/app/components/field/Richtext.vue +0 -26
  268. package/layers/ui/app/components/field/Slug.vue +0 -65
  269. package/layers/ui/app/components/field/Text.vue +0 -35
  270. package/layers/ui/app/components/field/Unsupported.vue +0 -34
  271. package/layers/ui/app/components/ui/Alert.vue +0 -50
  272. package/layers/ui/app/components/ui/Brand.vue +0 -22
  273. package/layers/ui/app/components/ui/Button.vue +0 -116
  274. package/layers/ui/app/components/ui/ButtonGroup.vue +0 -96
  275. package/layers/ui/app/components/ui/Checkbox.vue +0 -53
  276. package/layers/ui/app/components/ui/CheckboxGroup.vue +0 -51
  277. package/layers/ui/app/components/ui/Combobox.vue +0 -345
  278. package/layers/ui/app/components/ui/DatePicker.vue +0 -92
  279. package/layers/ui/app/components/ui/DateRangePicker.vue +0 -100
  280. package/layers/ui/app/components/ui/Dialog.vue +0 -107
  281. package/layers/ui/app/components/ui/EmptyState.vue +0 -46
  282. package/layers/ui/app/components/ui/Field.vue +0 -58
  283. package/layers/ui/app/components/ui/Fieldset.vue +0 -65
  284. package/layers/ui/app/components/ui/Icon.vue +0 -44
  285. package/layers/ui/app/components/ui/Menu.vue +0 -54
  286. package/layers/ui/app/components/ui/NumberInput.vue +0 -175
  287. package/layers/ui/app/components/ui/Richtext.vue +0 -141
  288. package/layers/ui/app/components/ui/RichtextToolbar.vue +0 -251
  289. package/layers/ui/app/components/ui/Select.vue +0 -52
  290. package/layers/ui/app/components/ui/TextInput.vue +0 -111
  291. package/layers/ui/app/components/ui/Textarea.vue +0 -39
  292. package/layers/ui/app/components/ui/TimeInput.vue +0 -18
  293. package/layers/ui/app/components/ui/Toasts.vue +0 -103
  294. package/layers/ui/app/components/ui/Tooltip.vue +0 -45
  295. package/layers/ui/app/composables/useAdminLang.ts +0 -8
  296. package/layers/ui/app/composables/useDragReorder.ts +0 -41
  297. package/layers/ui/app/composables/useEchoGuard.ts +0 -28
  298. package/layers/ui/app/composables/useLinkField.ts +0 -70
  299. package/layers/ui/app/composables/useRecordOptions.ts +0 -84
  300. package/layers/ui/app/composables/useRepeater.ts +0 -99
  301. package/layers/ui/app/composables/useT.ts +0 -32
  302. package/layers/ui/app/composables/useToast.ts +0 -46
  303. package/layers/ui/app/i18n/de.ts +0 -432
  304. package/layers/ui/app/i18n/en.ts +0 -434
  305. package/layers/ui/app/utils/boolean-field.ts +0 -10
  306. package/layers/ui/app/utils/field-component.ts +0 -22
  307. package/layers/ui/app/utils/field-empty.ts +0 -21
  308. package/layers/ui/app/utils/field-registry.ts +0 -36
  309. package/layers/ui/app/utils/humanize.ts +0 -14
  310. package/layers/ui/app/utils/icons.ts +0 -143
  311. package/layers/ui/app/utils/iso-date.ts +0 -18
  312. package/layers/ui/app/utils/localized.ts +0 -13
  313. package/layers/ui/app/utils/record-context.ts +0 -15
  314. package/layers/ui/app/utils/reorder.ts +0 -8
  315. package/layers/ui/app/utils/richtext-preserve-class.ts +0 -31
  316. package/layers/ui/app/utils/useFieldA11y.ts +0 -40
  317. package/layers/ui/nuxt.config.ts +0 -9
  318. package/nuxt.config.ts +0 -108
  319. package/scripts/consumer-template-ci.mjs +0 -160
  320. package/scripts/copy-create-payload.mjs +0 -49
  321. package/scripts/coverage-all.mjs +0 -163
  322. package/scripts/dashboard.mjs +0 -89
  323. package/scripts/hash-password.mjs +0 -29
  324. package/scripts/kestrel.mjs +0 -240
  325. package/scripts/lib/cli.mjs +0 -129
  326. package/scripts/lib/coverage-model.mjs +0 -51
  327. package/scripts/lib/password.mjs +0 -19
  328. package/scripts/lib/scaffold.mjs +0 -205
  329. package/scripts/port-graph.mjs +0 -350
  330. package/scripts/typecheck.mjs +0 -62
  331. package/templates/starter/README.md +0 -65
  332. package/templates/starter/_env.example +0 -17
  333. package/templates/starter/_gitignore +0 -26
  334. package/templates/starter/_package.json +0 -22
  335. package/templates/starter/app/app.vue +0 -7
  336. package/templates/starter/app/blocks/Prose.vue +0 -13
  337. package/templates/starter/app/layouts/default.vue +0 -6
  338. package/templates/starter/nuxt.config.ts +0 -23
  339. package/templates/starter/pnpm-workspace.yaml +0 -8
  340. 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>