@michaelthielemann/kestrel 4.1.2 → 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,95 +0,0 @@
1
- import { ref, computed } from 'vue'
2
- import type { Ref } from 'vue'
3
- import { useEchoGuard } from '../../../ui/app/composables/useEchoGuard'
4
- import type { SerializedBlock } from '@michaelthielemann/kestrel-core'
5
- import {
6
- type BlockRow,
7
- findInTree,
8
- updatePropById,
9
- removeById,
10
- removalRetarget,
11
- moveById,
12
- duplicateById,
13
- addBlock,
14
- } from '../utils/block-tree'
15
-
16
- /**
17
- * Reactive state for the 3-pane block editor: one nested `content` tree plus a single shared selection
18
- * (the stable `block.id`, or `null` for the page root). The tree pane and the fields pane both drive
19
- * the same instance — the tree selects + does structural ops, the fields pane edits the selected
20
- * block's props. Every mutation is id-addressed and immutable (see `block-tree.ts`) and re-emits the
21
- * whole array (so `useEditForm`'s `setField('content', …)` reconciliation still runs). `genId` is
22
- * injectable for deterministic tests; defaults to `crypto.randomUUID` (the server requires a non-empty id).
23
- */
24
- export function useBlockTree(
25
- model: Ref<unknown[] | null | undefined>,
26
- schemas: Ref<Record<string, SerializedBlock>>,
27
- genId: () => string = () => crypto.randomUUID(),
28
- // Routes an emit through the caller's own undo-history capture point instead of a plain `model.value =`
29
- // assignment, tagged with a per-op coalesce key (see `emit`) — so a structural op landing inside another
30
- // edit's coalesce window (e.g. a delete right after typing) still gets its own undo step.
31
- setContent?: (value: unknown[], coalesceAs: string) => void,
32
- ) {
33
- // JSON round-trip (not structuredClone — which throws on Vue reactive proxies): reads through the
34
- // proxy into detached plain objects. Block content is JSON by definition, so this is lossless here.
35
- const clone = (v: unknown): BlockRow[] => (Array.isArray(v) ? (JSON.parse(JSON.stringify(v)) as BlockRow[]) : [])
36
-
37
- const blocks = ref<BlockRow[]>(clone(model.value))
38
- const selectedId = ref<string | null>(null)
39
-
40
- function emit(coalesceAs: string): void {
41
- if (setContent) setContent(blocks.value, coalesceAs)
42
- else model.value = blocks.value
43
- }
44
-
45
- // Echo-guard against the tree's OWN emit (shared primitive): an external change (load / locale copy /
46
- // undo / redo) differs from the current tree, so it reseeds and drops a now-missing selection to root.
47
- useEchoGuard(model, () => blocks.value, (v) => {
48
- blocks.value = clone(v)
49
- if (selectedId.value !== null && !findInTree(blocks.value, selectedId.value)) selectedId.value = null
50
- }, [])
51
-
52
- const selectedBlock = computed<BlockRow | null>(() =>
53
- selectedId.value === null ? null : (findInTree(blocks.value, selectedId.value)?.block ?? null),
54
- )
55
-
56
- function select(id: string | null): void {
57
- selectedId.value = id
58
- }
59
-
60
- function setProp(id: string, key: string, value: unknown): void {
61
- blocks.value = updatePropById(blocks.value, id, key, value)
62
- // Coalesces a typing burst into that one field, same as an ordinary text field would.
63
- emit(`content:prop:${id}:${key}`)
64
- }
65
-
66
- function remove(id: string): void {
67
- const retarget = removalRetarget(blocks.value, id)
68
- blocks.value = removeById(blocks.value, id)
69
- // Heal the selection whenever it no longer exists — covers removing the selected block itself AND
70
- // removing an ANCESTOR of it (the whole subtree goes); land on the removed block's sibling/parent.
71
- if (selectedId.value !== null && !findInTree(blocks.value, selectedId.value)) selectedId.value = retarget
72
- emit(`content:remove:${id}`)
73
- }
74
-
75
- function move(id: string, dir: -1 | 1): void {
76
- blocks.value = moveById(blocks.value, id, dir)
77
- emit(`content:move:${id}`)
78
- }
79
-
80
- function duplicate(id: string): void {
81
- const { tree, newId } = duplicateById(blocks.value, id, genId)
82
- blocks.value = tree
83
- selectedId.value = newId
84
- emit(`content:duplicate:${id}`)
85
- }
86
-
87
- function add(parentId: string | null, slotName: string | null, type: string): void {
88
- const { tree, newId } = addBlock(blocks.value, parentId, slotName, type, schemas.value, genId)
89
- blocks.value = tree
90
- selectedId.value = newId
91
- emit(`content:add:${parentId}:${slotName}:${type}`)
92
- }
93
-
94
- return { blocks, selectedId, selectedBlock, select, setProp, remove, move, duplicate, add }
95
- }
@@ -1,7 +0,0 @@
1
- import type { SerializedBlock } from '@michaelthielemann/kestrel-core'
2
-
3
- // Thin wrapper over the shared cached-resource loader; keeps the named `blocks` return.
4
- export function useBlocks() {
5
- const { state: blocks, load } = useCachedResource<SerializedBlock>('kestrel-blocks', '/api/blocks')
6
- return { blocks, load }
7
- }
@@ -1,35 +0,0 @@
1
- // Generic cached-resource loader: a `useState`-backed list that fetches once and is shared by every
2
- // caller. Backs `useBlocks` / `useCollections` (structurally identical loaders).
3
- //
4
- // SSR isolation: the in-flight (dedup) promise is keyed first by the per-request Nuxt app (a WeakMap,
5
- // so it can't leak across SSR requests) AND then by `stateKey` — otherwise two different resources
6
- // loading concurrently in ONE request would share a single promise and cross-contaminate (one
7
- // resolves with the other's data).
8
- const inflight = new WeakMap<object, Map<string, Promise<unknown>>>()
9
-
10
- export function useCachedResource<T>(stateKey: string, url: string) {
11
- const state = useState<T[] | null>(stateKey, () => null)
12
- const nuxtApp = useNuxtApp()
13
-
14
- async function load(): Promise<T[]> {
15
- if (state.value) return state.value
16
- let perApp = inflight.get(nuxtApp)
17
- if (!perApp) {
18
- perApp = new Map()
19
- inflight.set(nuxtApp, perApp)
20
- }
21
- let pending = perApp.get(stateKey) as Promise<T[]> | undefined
22
- if (!pending) {
23
- pending = $fetch<{ data: T[] }>(url)
24
- .then(({ data }) => {
25
- state.value = data
26
- return data
27
- })
28
- .finally(() => perApp!.delete(stateKey))
29
- perApp.set(stateKey, pending)
30
- }
31
- return pending
32
- }
33
-
34
- return { state, load }
35
- }
@@ -1,102 +0,0 @@
1
- import { ref, toValue, type MaybeRefOrGetter } from 'vue'
2
- import { buildDeleteReport, type BatchAction, type BatchDeleteReport, type BatchPipelineResult, type BulkResult, type ReferrerCounts } from '../utils/collection-ops'
3
- import type { SerializedAction } from '@michaelthielemann/kestrel-core'
4
-
5
- /** Which write pipeline each batch action runs, and the body it takes. A publish/unpublish is an
6
- * `updateMany` with a status patch — the same pipeline any other field-wide edit would use. */
7
- const BATCH_PIPELINE: Record<BatchAction, { op: string, patch?: { status: 'published' | 'draft' } }> = {
8
- delete: { op: 'deleteMany' },
9
- duplicate: { op: 'duplicate' },
10
- publish: { op: 'updateMany', patch: { status: 'published' } },
11
- unpublish: { op: 'updateMany', patch: { status: 'draft' } },
12
- }
13
-
14
- /**
15
- * Client orchestration for the collection batch operations — ONE module, two entry points: a row action
16
- * passes a single id, the bulk bar passes many. Every mutating call runs a batch pipeline
17
- * (`POST /api/{collection}/{pipeline}`); the server does the batching, the client never loops N requests.
18
- * Mirrors media/useMediaOps (busy/error + an onChanged refetch hook).
19
- */
20
- export function useCollectionOps(collection: MaybeRefOrGetter<string>, onChanged?: () => void | Promise<void>) {
21
- const { t } = useT()
22
- const busy = ref(false)
23
- const error = ref<string | null>(null)
24
- const name = () => toValue(collection)
25
-
26
- async function bulk(action: BatchAction, ids: number[]): Promise<BulkResult> {
27
- busy.value = true
28
- error.value = null
29
- try {
30
- const { op, patch } = BATCH_PIPELINE[action]
31
- const res = await $fetch<BatchPipelineResult>(`/api/${name()}/${op}`, { method: 'POST', body: { ids, ...(patch ? { patch } : {}) } })
32
- await onChanged?.()
33
- // `duplicate` answers with the CREATED rows, the two batch ops with a count + the ids they touched;
34
- // both fold into the one shape the list UI reports.
35
- return Array.isArray(res)
36
- ? { action, count: res.length, ids: res.map((row) => Number(row.id)) }
37
- : { action, count: res.count, ids: res.ids }
38
- } catch (e) {
39
- error.value = (e as { statusMessage?: string })?.statusMessage ?? t('list.opFailed')
40
- throw e
41
- } finally {
42
- busy.value = false
43
- }
44
- }
45
-
46
- /** Read-only pre-delete summary (referrer aggregate). Best-effort: sets `error` and rethrows on failure
47
- * so the caller can decide whether to still proceed (deletion is not blocked on the warning lookup). */
48
- async function previewDelete(ids: number[]): Promise<BatchDeleteReport> {
49
- error.value = null
50
- try {
51
- const res = await $fetch<ReferrerCounts>(`/api/${name()}/referrers`, { query: { ids: ids.join(',') } })
52
- // The endpoint flags whether the index lookup ran; carry that through rather than inferring safety from
53
- // an empty `counts`. A missing body is indeterminate too — it is no evidence that nothing links here.
54
- return buildDeleteReport(ids, res?.counts ?? {}, res != null && res.checked !== false)
55
- } catch (e) {
56
- error.value = (e as { statusMessage?: string })?.statusMessage ?? t('list.refCheckFailed')
57
- throw e
58
- }
59
- }
60
-
61
- /**
62
- * Run a schema-driven custom action (a consumer's `definePipeline` beyond the built-in bulk set). The
63
- * response shape is whatever that pipeline answers with — unlike the built-ins, generic rendering has no
64
- * contract to decode, so the reported result is simply the ids the caller asked to act on.
65
- */
66
- async function runCustomAction(action: SerializedAction, ids: number[]): Promise<BulkResult> {
67
- busy.value = true
68
- error.value = null
69
- try {
70
- const record = action.kind === 'record'
71
- const url = record ? `/api/${name()}/${action.name}/${ids[0]}` : `/api/${name()}/${action.name}`
72
- await $fetch(url, { method: 'POST', body: record ? undefined : { ids } })
73
- await onChanged?.()
74
- return { action: action.name, count: ids.length, ids }
75
- } catch (e) {
76
- error.value = (e as { statusMessage?: string })?.statusMessage ?? t('list.opFailed')
77
- throw e
78
- } finally {
79
- busy.value = false
80
- }
81
- }
82
-
83
- const confirmDelete = (ids: number[]) => bulk('delete', ids)
84
- const duplicate = (ids: number[]) => bulk('duplicate', ids)
85
- /**
86
- * Persist a new status and, for a publish, write the static output too (ADR-0008). Two calls because
87
- * they are two things: `updateMany` owns the DB write (validation, all-or-nothing, write events),
88
- * `/api/publish` owns the render. Unpublishing needs no second call — the write event prunes the pages
89
- * on its own, since taking a page offline must never wait for a separate action.
90
- */
91
- const setStatus = async (ids: number[], status: 'published' | 'draft'): Promise<BulkResult> => {
92
- const res = await bulk(status === 'published' ? 'publish' : 'unpublish', ids)
93
- if (status === 'published') {
94
- // Best-effort: the records ARE published (the state the list shows); a failed render surfaces on the
95
- // record's own status lamp, and re-pressing Publish retries it.
96
- await $fetch('/api/publish', { method: 'POST', body: { collection: name(), ids } }).catch(() => null)
97
- }
98
- return res
99
- }
100
-
101
- return { busy, error, previewDelete, confirmDelete, duplicate, setStatus, runCustomAction }
102
- }
@@ -1,6 +0,0 @@
1
- import type { SerializedCollection } from '@michaelthielemann/kestrel-core'
2
-
3
- export function useCollections() {
4
- const { state: collections, load } = useCachedResource<SerializedCollection>('kestrel-collections', '/api/collections')
5
- return { collections, load }
6
- }
@@ -1,23 +0,0 @@
1
- export interface ContentLocales {
2
- /** Website/content locales (NOT the admin-UI language) — what the editor LocaleBar offers. */
3
- locales: string[]
4
- primary: string
5
- /** Whether the primary locale is prefixed in public URLs too (`/en/about`); the `localePath` flag. */
6
- prefixPrimary: boolean
7
- }
8
-
9
- /** Resolve the configured content locales from a `runtimeConfig.public`-shaped object. Pure, so the
10
- * fallback/primary logic is unit-testable without a Nuxt context. The `['en']` fallback is a safe
11
- * last resort for an absent/empty config (nuxt.config always sets `public.locales`): primary-only is
12
- * a safer guess than assuming a second locale exists, so it intentionally differs from the server's
13
- * `['en','de']` *default* (which applies only when KESTREL_LOCALES itself is unset). */
14
- export function pickContentLocales(pub: { locales?: unknown; primaryLocale?: unknown; prefixPrimary?: unknown }): ContentLocales {
15
- const locales = Array.isArray(pub.locales) && pub.locales.length ? pub.locales.map(String) : ['en']
16
- const primary =
17
- typeof pub.primaryLocale === 'string' && locales.includes(pub.primaryLocale) ? pub.primaryLocale : locales[0]!
18
- return { locales, primary, prefixPrimary: pub.prefixPrimary === true }
19
- }
20
-
21
- export function useContentLocales(): ContentLocales {
22
- return pickContentLocales(useRuntimeConfig().public as { locales?: unknown; primaryLocale?: unknown; prefixPrimary?: unknown })
23
- }