@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,20 +0,0 @@
1
- import { migrateRevisions, useDb, type MigrateRevisionsResult } from '@michaelthielemann/kestrel-core'
2
-
3
- // Thin Nitro wrapper (defineTask is a runtime global) around the core, independently-unit-tested
4
- // `migrateRevisions` (db/revision-migration.ts) — same sync/task split as db:migrate (schema/sync.ts +
5
- // tasks/db/migrate.ts). Dev: GET/POST http://localhost:3000/_nitro/tasks/db:migrate-revisions ; prod: call
6
- // runTask('db:migrate-revisions', { payload }) from inside the process — the built node-server has no task
7
- // CLI/endpoint. Payload: {force:true} required, or the run refuses.
8
- export default defineTask<MigrateRevisionsResult[]>({
9
- meta: {
10
- name: 'db:migrate-revisions',
11
- description: 'Seed revision 1 for every existing row with no revision history yet. Requires {"force":true}.',
12
- },
13
- run({ payload }) {
14
- const p = (payload ?? {}) as { force?: boolean }
15
- const result = migrateRevisions(useDb(), { force: p.force === true })
16
- const totalSeeded = result.reduce((n, r) => n + r.seeded, 0)
17
- console.info(`[kestrel] db:migrate-revisions seeded ${totalSeeded} revision(s) across ${result.length} collection(s)`)
18
- return { result }
19
- },
20
- })
@@ -1,50 +0,0 @@
1
- import collections from '#kestrel/collections'
2
- import schemaTables from '#kestrel/schema-tables'
3
- import type { AnySQLiteTable } from 'drizzle-orm/sqlite-core'
4
- import { describeOp, desiredFromCollections, isDestructive, planOps, sqlite, syncSchema, useDb } from '@michaelthielemann/kestrel-core'
5
- import type { BuiltCollection, CollectionDef, SyncDb } from '@michaelthielemann/kestrel-core'
6
- import { resolveServerKestrel, serverRuntimeConfig } from '../../utils/server-config'
7
- // Explicit schema-migrate step for production (ADR-0002). Dev auto-syncs additively at boot
8
- // (02.schema-sync); prod boot never auto-DDLs, so an operator runs this task. Triggering (Nuxt 4.4 /
9
- // Nitro 2.13 — there is NO `nuxi task run`):
10
- // - dev: GET http://localhost:3000/_nitro/tasks/db:migrate (the dev-only task route)
11
- // - prod: call runTask('db:migrate', { payload }) from inside the process (an authenticated route or a
12
- // cron scheduledTask); the built node-server has no task CLI/endpoint.
13
- // Payloads: {check:true}=dry-run (report pending DDL, change nothing); {force:true}=also apply REBUILDS
14
- // (column drop/type change); {drop:["old_table"]}=drop a specific table (never bundled in force).
15
- // Destructive ops are reported-but-withheld unless explicitly opted into; `force` never drops a table.
16
- // The two run() branches return different result shapes (dry-run report vs applied/skipped); the explicit
17
- // union is the task's result type so neither branch is forced to match the other.
18
- type MigrateResult =
19
- | { check: boolean; pending: string[]; destructive: string[] }
20
- | { applied: string[]; skipped: string[] }
21
-
22
- export default defineTask<MigrateResult>({
23
- meta: { name: 'db:migrate', description: 'Apply schema changes; {check:true}=dry-run, {force:true}=rebuilds, {drop:[names]}=drop tables' },
24
- run({ payload }) {
25
- const p = (payload ?? {}) as { check?: boolean; force?: boolean; drop?: string[] }
26
- const desired = desiredFromCollections(collections as (CollectionDef | BuiltCollection)[], {
27
- // Same toggle source as the registry plugin, so runtime surface and schema agree (see 02.schema-sync).
28
- toggles: (serverRuntimeConfig()?.kestrel?.collections ?? resolveServerKestrel().collections) as Record<string, boolean>,
29
- extraTables: schemaTables as AnySQLiteTable[],
30
- })
31
- const client = (useDb() as unknown as { $client: SyncDb }).$client
32
- // SQLite is the only backend today; this is the seam where a config-driven `resolveDialect()` would go.
33
- const dialect = sqlite
34
-
35
- if (p.check === true) {
36
- const ops = planOps(client, desired, dialect)
37
- const destructive = ops.filter(isDestructive).map(describeOp)
38
- console.info(`[kestrel] db:migrate dry-run: ${ops.length} pending op(s), ${destructive.length} destructive`)
39
- return { result: { check: true, pending: dialect.render(ops), destructive } }
40
- }
41
-
42
- const { applied, skipped } = syncSchema(client, desired, {
43
- allowDestructive: p.force === true,
44
- dropTables: Array.isArray(p.drop) ? p.drop : undefined,
45
- }, dialect)
46
- if (skipped.length) console.warn(`[kestrel] db:migrate withheld ${skipped.length} destructive change(s) — opt in with {"force":true} (rebuilds) / {"drop":[…]} (drops):\n - ${skipped.map(describeOp).join('\n - ')}`)
47
- console.info(`[kestrel] db:migrate applied ${applied.length} statement(s)`)
48
- return { result: { applied, skipped: skipped.map(describeOp) } }
49
- },
50
- })
@@ -1,81 +0,0 @@
1
- import kestrelConfig from '../../../../kestrel.config'
2
- import { resolveKestrel, type ResolvedKestrel } from '@michaelthielemann/kestrel-core'
3
-
4
- /**
5
- * The Nitro runtimeConfig, or `undefined` for non-Nitro callers (node scripts, build) so they don't
6
- * throw on `useRuntimeConfig()`. Server utils prefer the values the kestrel module put here (from the
7
- * consumer's `kestrel: {}`), falling back to `resolveServerKestrel()` when it isn't populated.
8
- */
9
- export function serverRuntimeConfig(): { kestrel?: Record<string, unknown>; media?: Record<string, unknown>; public?: Record<string, unknown> } | undefined {
10
- return typeof useRuntimeConfig === 'function'
11
- ? (useRuntimeConfig() as { kestrel?: Record<string, unknown>; media?: Record<string, unknown>; public?: Record<string, unknown> })
12
- : undefined
13
- }
14
-
15
- /** Resolve Kestrel's own config file + env — the fallback when runtimeConfig isn't populated. */
16
- export function resolveServerKestrel(): ReturnType<typeof resolveKestrel> {
17
- return resolveKestrel(kestrelConfig, process.env, process.cwd())
18
- }
19
-
20
- /**
21
- * The full `ResolvedKestrel` shape, merging every server util's own `runtimeConfig ?? resolveServerKestrel()`
22
- * precedence into ONE call — the boot-time config-wiring plugin (`00.config.ts`) calls this once and
23
- * pushes the result into the (eventually package-side) config provider, so package code never calls
24
- * `useRuntimeConfig()` itself. Per-field precedence matches each field's existing reader exactly (the
25
- * kestrel module writes a CLOSED set of fields into `runtimeConfig.kestrel`/`.public` — see
26
- * `layers/core/modules/kestrel/index.ts`; anything it doesn't write there falls through to the
27
- * `resolveServerKestrel()` base for every reader, unchanged from before this function existed).
28
- */
29
- export function resolveServerKestrelConfig(): ResolvedKestrel {
30
- const rc = serverRuntimeConfig()
31
- const fallback = resolveServerKestrel()
32
- const srv = (rc?.kestrel ?? {}) as Partial<Record<string, unknown>>
33
- const pub = rc?.public as { locales?: string[]; primaryLocale?: string; prefixPrimary?: boolean } | undefined
34
- // `runtimeConfig.media` is a TOP-LEVEL key (sibling to `.kestrel`/`.public`, not nested under `.kestrel`
35
- // — see `layers/core/modules/kestrel/index.ts`'s `rc.media = {...}`), and it is the ONLY place the S3
36
- // credentials + every Nitro `NUXT_MEDIA_*`/`NUXT_MEDIA_S3_*` env override (docs/guide/configuration.md) ever
37
- // land — `resolveKestrel`'s own pure computation never sees them. Merged in wholesale (undefined ⇒ the
38
- // module never ran ⇒ fall through to the pure fallback), mirroring `output`'s existing merge below.
39
- const rcMedia = rc?.media as {
40
- driver?: string
41
- maxUploadBytes?: number
42
- allowedMimes?: string
43
- imagePolicy?: ResolvedKestrel['media']['imagePolicy']
44
- local?: { dir?: string; baseUrl?: string }
45
- s3?: { bucket?: string; region?: string; endpoint?: string; prefix?: string; publicBaseUrl?: string; accessKeyId?: string; secretAccessKey?: string; sessionToken?: string }
46
- } | undefined
47
- // The locale trio is populated together or not at all (see `locale.ts`'s original `resolved()`) — a
48
- // partial `runtimeConfig.public` (e.g. `locales` present but `primaryLocale` absent) falls through to
49
- // the fallback for all three rather than mixing sources mid-trio.
50
- const locale = pub?.locales?.length && pub.primaryLocale
51
- ? { supportedLocales: pub.locales, primaryLocale: pub.primaryLocale, prefixPrimary: pub.prefixPrimary === true }
52
- : { supportedLocales: fallback.supportedLocales, primaryLocale: fallback.primaryLocale, prefixPrimary: fallback.prefixPrimary }
53
- return {
54
- ...fallback,
55
- ...locale,
56
- dbPath: (srv.dbPath as string | undefined) ?? fallback.dbPath,
57
- siteUrl: (srv.siteUrl as string | undefined) ?? fallback.siteUrl,
58
- siteName: (srv.siteName as string | undefined) ?? fallback.siteName,
59
- siteDescription: (srv.siteDescription as string | undefined) ?? fallback.siteDescription,
60
- media: rcMedia ? {
61
- dir: rcMedia.local?.dir ?? fallback.media.dir,
62
- baseUrl: rcMedia.local?.baseUrl ?? fallback.media.baseUrl,
63
- driver: (rcMedia.driver as 'local' | 's3' | undefined) ?? fallback.media.driver,
64
- maxUploadBytes: rcMedia.maxUploadBytes ?? fallback.media.maxUploadBytes,
65
- allowedMimes: rcMedia.allowedMimes ?? fallback.media.allowedMimes,
66
- imagePolicy: rcMedia.imagePolicy ?? fallback.media.imagePolicy,
67
- s3: { ...fallback.media.s3, ...rcMedia.s3 },
68
- } : fallback.media,
69
- collections: (srv.collections as ResolvedKestrel['collections'] | undefined) ?? fallback.collections,
70
- aiDisclosure: (srv.aiDisclosure as ResolvedKestrel['aiDisclosure'] | undefined) ?? fallback.aiDisclosure,
71
- seo: (srv.seo as ResolvedKestrel['seo'] | undefined) ?? fallback.seo,
72
- delivery: (srv.delivery as ResolvedKestrel['delivery'] | undefined) ?? fallback.delivery,
73
- deliveryExempt: (srv.deliveryExempt as string[] | undefined) ?? fallback.deliveryExempt,
74
- output: (srv.output as ResolvedKestrel['output'] | undefined) ?? fallback.output,
75
- // `revisions` is NOT written into `runtimeConfig.kestrel` by the module today (pre-existing —
76
- // unrelated to this seam), so this reads as `fallback.revisions` in every real boot right now; kept
77
- // symmetric with every other field rather than special-cased, so a future module fix (writing
78
- // `srv.revisions`) is picked up here for free.
79
- revisions: (srv.revisions as ResolvedKestrel['revisions'] | undefined) ?? fallback.revisions,
80
- }
81
- }
@@ -1 +0,0 @@
1
- export default defineNuxtConfig({})
@@ -1,7 +0,0 @@
1
- import { registerPipeline } from '@michaelthielemann/kestrel-core'
2
- import { buildBlocksPipelines } from '@michaelthielemann/kestrel-fields'
3
-
4
- // Registration only — nothing resolves a pipeline here; the registry is read on the first request.
5
- export default defineNitroPlugin(() => {
6
- for (const def of buildBlocksPipelines()) registerPipeline(def)
7
- })
@@ -1,11 +0,0 @@
1
- import { registerPopulator } from '@michaelthielemann/kestrel-core'
2
- import { buildFieldTreePopulator } from '@michaelthielemann/kestrel-fields'
3
-
4
- // The single global row populator: walks a record's fields + block tree and dispatches each field to its
5
- // per-type populator (media / link / richtext / relation), each registered by its owning layer. It recurses
6
- // repeater entries, block props, and slots, so nested references resolve everywhere — not just at the top
7
- // level. Field populators are looked up at read time, so this plugin's order vs the layer plugins that
8
- // register them does not matter.
9
- export default defineNitroPlugin(() => {
10
- registerPopulator(buildFieldTreePopulator())
11
- })
@@ -1,94 +0,0 @@
1
- <script setup lang="ts">
2
- import { useBlurUp } from '../composables/useBlurUp'
3
- import { useMediaVariant } from '../composables/useMediaVariant'
4
- import { aiSourceTypeLabel } from '../utils/ai-disclosure'
5
- import type { ResolvedMedia } from '@michaelthielemann/kestrel-media'
6
- import type { VariantFit, VariantFormat } from '@michaelthielemann/kestrel-core'
7
-
8
- // The engine's responsive-image component: it renders a <picture> from the media's generated derivatives
9
- // AND its usage declares (to the generate-time scan) which sizes/formats are actually needed — "not in the
10
- // code ⇒ not generated". One usage may declare MULTIPLE `widths` (proportional) and/or a fixed `crop`, plus
11
- // named config `preset`s, crossed with `formats`. `priority` marks the LCP image (eager + high fetchpriority;
12
- // no `sizes=auto`, which only applies to lazy images). `sizes="auto"` degrades gracefully (see resolveSizes).
13
- const props = defineProps<{
14
- media: ResolvedMedia | null | undefined
15
- widths?: number[]
16
- crop?: { width: number; height: number; fit?: VariantFit }
17
- preset?: string | string[]
18
- formats?: VariantFormat[]
19
- sizes?: string
20
- priority?: boolean
21
- /** Opt into a structurally-present but UNSTYLED EU AI Act disclosure badge (`.kestrel-img__ai-badge`),
22
- * rendered only when `media.aiDisclosure` is set. Off by default: Kestrel must never publish a claim
23
- * the consumer did not ask for, in a place/language/style it cannot know. The alternative escape hatch
24
- * is to read `media.aiDisclosure` and render your own element — see docs/guide/ai-disclosure.md. */
25
- aiBadge?: boolean
26
- }>()
27
-
28
- const { model } = useMediaVariant(
29
- () => props.media,
30
- () => ({ widths: props.widths, crop: props.crop, preset: props.preset, formats: props.formats, sizes: props.sizes }),
31
- { priority: () => props.priority ?? false },
32
- )
33
- const { imgEl, animate, placeholderStyle, onLoad } = useBlurUp(() => props.media?.thumbhash)
34
- </script>
35
-
36
- <template>
37
- <picture v-if="model">
38
- <source v-for="s in model.sources" :key="s.type" :type="s.type" :srcset="s.srcset" :sizes="model.sizes">
39
- <img
40
- ref="imgEl"
41
- :src="model.src"
42
- :sizes="model.sizes"
43
- :width="model.width ?? undefined"
44
- :height="model.height ?? undefined"
45
- :alt="model.alt"
46
- :class="{ 'kestrel-img--in': animate }"
47
- :style="placeholderStyle"
48
- :loading="priority ? 'eager' : 'lazy'"
49
- :fetchpriority="priority ? 'high' : undefined"
50
- decoding="async"
51
- @load="onLoad"
52
- >
53
- <span
54
- v-if="aiBadge && media?.aiDisclosure"
55
- class="kestrel-img__ai-badge"
56
- :data-ai-source-type="media.aiDisclosure.sourceType"
57
- >{{ media.aiDisclosure.note ?? aiSourceTypeLabel(media.aiDisclosure.sourceType) }}</span>
58
- </picture>
59
- </template>
60
-
61
- <style scoped>
62
- /* Sharpen-in: one-shot blur→crisp on a genuine async load (JS-driven, so SSR / no-JS / cached just show
63
- the image). Same treatment as the demo MediaImage. */
64
- @keyframes kestrel-img-sharpen {
65
- from { filter: blur(8px); }
66
- to { filter: blur(0); }
67
- }
68
- .kestrel-img--in {
69
- animation: kestrel-img-sharpen 0.5s ease-out;
70
- }
71
- @media (prefers-reduced-motion: reduce) {
72
- .kestrel-img--in {
73
- animation: none;
74
- }
75
- }
76
- </style>
77
-
78
- <!-- Deliberately UNSCOPED: a scoped rule carries the `[data-v-*]` attribute selector, which out-specifies
79
- a consumer stylesheet targeting `.kestrel-img__ai-badge` and would make the badge un-restylable.
80
- Layout only — no color, background, border, radius or font — so nothing is visible as a "badge" until
81
- the consumer's own CSS says so. Scoped to pictures that actually contain one, so a consumer who never
82
- sets `ai-badge` gets no rule at all. -->
83
- <style>
84
- picture:has(> .kestrel-img__ai-badge) {
85
- position: relative;
86
- display: inline-block;
87
- }
88
- .kestrel-img__ai-badge {
89
- position: absolute;
90
- inset-block-end: 0;
91
- inset-inline-start: 0;
92
- pointer-events: none;
93
- }
94
- </style>
@@ -1,35 +0,0 @@
1
- <script setup lang="ts">
2
- import { computed } from 'vue'
3
- import type { Conflict } from '../utils/ops'
4
-
5
- const props = defineProps<{ open: boolean; conflicts: Conflict[]; type: 'move' | 'copy'; dest: string; busy?: boolean; error?: string | null }>()
6
- const emit = defineEmits<{ resolve: ['skip' | 'overwrite' | 'rename']; 'update:open': [boolean] }>()
7
-
8
- const { t } = useT()
9
- const title = computed(() => props.type === 'move' ? t('media.conflictTitleMove') : t('media.conflictTitleCopy'))
10
- </script>
11
-
12
- <template>
13
- <KestrelUiDialog :open="open" :title="title" @update:open="(v) => emit('update:open', v)">
14
- <p>{{ t('media.conflictItems', { count: conflicts.length, dest: dest || t('media.rootFolder') }) }}</p>
15
- <ul class="media-conflict__list">
16
- <li v-for="c in conflicts" :key="c.targetPath">{{ c.targetPath }} — {{ c.type === 'folder-exists' ? t('media.conflictFolderExists') : t('media.conflictFileExists') }}</li>
17
- </ul>
18
- <KestrelUiAlert v-if="error" variant="error">{{ error }}</KestrelUiAlert>
19
- <template #footer>
20
- <KestrelUiButton :disabled="busy" @click="emit('update:open', false)">{{ t('common.cancel') }}</KestrelUiButton>
21
- <KestrelUiButton :disabled="busy" @click="emit('resolve', 'skip')">{{ t('media.skip') }}</KestrelUiButton>
22
- <KestrelUiButton :disabled="busy" @click="emit('resolve', 'overwrite')">{{ t('media.replace') }}</KestrelUiButton>
23
- <KestrelUiButton variant="primary" :disabled="busy" @click="emit('resolve', 'rename')">{{ t('media.keepBoth') }}</KestrelUiButton>
24
- </template>
25
- </KestrelUiDialog>
26
- </template>
27
-
28
- <style lang="scss" scoped>
29
- .media-conflict__list {
30
- margin: var(--space-2) 0 0;
31
- padding-left: var(--space-4);
32
- font-size: var(--text-sm);
33
- color: var(--color-text-muted);
34
- }
35
- </style>
@@ -1,52 +0,0 @@
1
- <script setup lang="ts">
2
- import { computed } from 'vue'
3
- import { humanizeSize } from '../utils/library'
4
- import type { DeleteReport } from '../utils/ops'
5
-
6
- const props = defineProps<{ open: boolean; report: DeleteReport | null; names: Record<number, string>; busy?: boolean; error?: string | null }>()
7
- const emit = defineEmits<{ confirm: []; 'update:open': [boolean] }>()
8
-
9
- const { t } = useT()
10
-
11
- const usageLines = computed(() => {
12
- const out: { file: string; refs: string }[] = []
13
- for (const [id, refs] of Object.entries(props.report?.usages ?? {})) {
14
- if (refs.length) out.push({ file: props.names[Number(id)] ?? `#${id}`, refs: refs.map((u) => `${u.collection} #${u.recordId} (${u.field})`).join(', ') })
15
- }
16
- return out
17
- })
18
- </script>
19
-
20
- <template>
21
- <KestrelUiDialog :open="open" :title="t('common.delete')" @update:open="(v) => emit('update:open', v)">
22
- <p v-if="report">
23
- {{ t('media.deleteSummary', {
24
- files: report.summary.files,
25
- filesUnit: report.summary.files === 1 ? t('media.file') : t('media.files'),
26
- folders: report.summary.folders,
27
- foldersUnit: report.summary.folders === 1 ? t('media.folder') : t('media.folders'),
28
- size: humanizeSize(report.summary.totalBytes)
29
- }) }}
30
- </p>
31
- <div v-if="usageLines.length" class="media-delete__usages">
32
- <p>{{ t('media.usageWarning') }}</p>
33
- <ul>
34
- <li v-for="u in usageLines" :key="u.file">{{ u.file }} → {{ u.refs }}</li>
35
- </ul>
36
- </div>
37
- <KestrelUiAlert v-if="error" variant="error">{{ error }}</KestrelUiAlert>
38
- <template #footer>
39
- <KestrelUiButton variant="ghost" :disabled="busy" @click="emit('update:open', false)">{{ t('common.cancel') }}</KestrelUiButton>
40
- <KestrelUiButton variant="danger" :disabled="busy" @click="emit('confirm')">{{ t('common.delete') }}</KestrelUiButton>
41
- </template>
42
- </KestrelUiDialog>
43
- </template>
44
-
45
- <style lang="scss" scoped>
46
- .media-delete__usages {
47
- margin-top: var(--space-3);
48
- color: var(--color-danger);
49
- font-size: var(--text-sm);
50
- ul { margin: var(--space-1) 0 0; padding-left: var(--space-4); }
51
- }
52
- </style>
@@ -1,113 +0,0 @@
1
- <script setup lang="ts">
2
- import { itemKey, humanizeSize, type LibraryFile, type LibraryItem } from '../utils/library'
3
- defineProps<{ items: LibraryItem[]; isSelected: (item: LibraryItem) => boolean; dropTargetPath?: string | null; parentPath?: string | null; upLabel?: string }>()
4
- const emit = defineEmits<{ navigate: [string]; select: [LibraryItem, { toggle: boolean; range: boolean }]; open: [LibraryItem]; dragstart: [LibraryItem, DragEvent]; dragend: [] }>()
5
- const mods = (e: MouseEvent) => ({ toggle: e.ctrlKey || e.metaKey, range: e.shiftKey })
6
- // Plain click opens the folder; modifier-click selects it so folders can join the selection.
7
- function onFolder(item: LibraryItem, path: string, e: MouseEvent) {
8
- if (e.ctrlKey || e.metaKey || e.shiftKey) emit('select', item, mods(e))
9
- else emit('navigate', path)
10
- }
11
- function onFile(item: LibraryItem, e: MouseEvent) { emit('select', item, mods(e)) }
12
- const ext = (filename: string) => (filename.split('.').pop() ?? '').toUpperCase()
13
- // Hover meta: dimensions (when known) · size — the same facts the table view shows in its columns.
14
- function fileMeta(f: LibraryFile): string {
15
- const parts: string[] = []
16
- if (f.width && f.height) parts.push(`${f.width}×${f.height}`)
17
- parts.push(humanizeSize(f.size))
18
- return parts.join(' · ')
19
- }
20
- </script>
21
-
22
- <template>
23
- <ul class="media-grid">
24
- <li v-if="parentPath != null" class="media-grid__cell">
25
- <!-- Pure up-navigation; not selectable/draggable (kept out of `items`). -->
26
- <button type="button" class="media-grid__tile media-grid__tile--folder" data-test="folder-up"
27
- :aria-label="upLabel" @click="emit('navigate', parentPath)">
28
- <span class="media-grid__thumb-wrap">
29
- <span class="media-grid__thumb media-grid__thumb--folder" aria-hidden="true"><KestrelUiIcon name="folder" :size="40" /></span>
30
- </span>
31
- <span class="media-grid__name">..</span>
32
- </button>
33
- </li>
34
- <li v-for="item in items" :key="itemKey(item)" class="media-grid__cell">
35
- <button v-if="item.type === 'folder'" type="button" class="media-grid__tile media-grid__tile--folder"
36
- :data-test="`folder-${item.folder.path}`" :aria-pressed="isSelected(item)"
37
- :data-drop-folder="item.folder.path"
38
- :class="{ 'is-selected': isSelected(item), 'is-drop-target': item.folder.path === dropTargetPath }"
39
- draggable="true"
40
- @click="onFolder(item, item.folder.path, $event)"
41
- @keydown.space.prevent="emit('select', item, { toggle: true, range: false })"
42
- @dragstart="(e) => emit('dragstart', item, e)"
43
- @dragend="emit('dragend')">
44
- <span class="media-grid__thumb-wrap">
45
- <span class="media-grid__thumb media-grid__thumb--folder" aria-hidden="true"><KestrelUiIcon name="folder" :size="40" /></span>
46
- <span v-if="item.folder.size != null" class="media-grid__meta" aria-hidden="true">{{ humanizeSize(item.folder.size) }}</span>
47
- </span>
48
- <span class="media-grid__name">{{ item.folder.name }}</span>
49
- </button>
50
- <button v-else type="button" class="media-grid__tile media-grid__tile--file"
51
- :data-test="`file-${item.file.id}`" :aria-pressed="isSelected(item)"
52
- :data-file-id="item.file.id"
53
- :class="{ 'is-selected': isSelected(item) }"
54
- draggable="true"
55
- @click="onFile(item, $event)"
56
- @dblclick="emit('open', item)"
57
- @keydown.enter.prevent="emit('open', item)"
58
- @dragstart="(e) => emit('dragstart', item, e)"
59
- @dragend="emit('dragend')">
60
- <span class="media-grid__thumb-wrap">
61
- <!-- Show a thumbnail for any image WITH a source: media files carry `srcset` (responsive derivatives),
62
- while a consumer feeding object-URLs (e.g. the secure gallery) has only `src`. Non-images → badge. -->
63
- <KestrelMediaThumb v-if="item.file.mime?.startsWith('image/') && (item.file.srcset || item.file.src)" class="media-grid__thumb"
64
- :src="item.file.src" :srcset="item.file.srcset" sizes="160px" :alt="item.file.alt ?? item.file.filename" :thumbhash="item.file.thumbhash" />
65
- <span v-else class="media-grid__badge" aria-hidden="true">{{ ext(item.file.filename) }}</span>
66
- <span class="media-grid__meta" aria-hidden="true">{{ fileMeta(item.file) }}</span>
67
- <!-- Optional per-file overlay (e.g. proofing colour flags). Empty by default → nothing rendered. -->
68
- <span class="media-grid__overlay"><slot name="file-overlay" :file="item.file" /></span>
69
- </span>
70
- <span class="media-grid__name">{{ item.file.filename }}</span>
71
- </button>
72
- </li>
73
- </ul>
74
- </template>
75
-
76
- <style lang="scss" scoped>
77
- .media-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr)); gap: var(--space-3); list-style: none; padding: 0; margin: 0; }
78
- .media-grid__tile { display: flex; flex-direction: column; gap: var(--space-2); width: 100%; background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: var(--space-2); cursor: pointer; text-align: left; }
79
- .media-grid__tile:hover { border-color: var(--color-border-strong); }
80
- // Selection/drop/focus rings are INSET (border-color + inset shadow, or a negative outline-offset) so a
81
- // scrolling ancestor never clips them — matches MediaTable. An outset outline (positive offset) on an
82
- // edge tile is sliced off by the library's overflow.
83
- .media-grid__tile.is-selected { border-color: var(--color-primary); box-shadow: inset 0 0 0 1px var(--color-primary); }
84
- .media-grid__tile.is-drop-target { outline: 2px dashed var(--color-primary); outline-offset: -2px; background: var(--color-surface); }
85
- .media-grid__tile:focus-visible { outline: 2px solid var(--color-focus); outline-offset: -2px; }
86
- .media-grid__thumb-wrap { position: relative; width: 100%; }
87
- .media-grid__overlay { position: absolute; top: var(--space-1); left: var(--space-1); display: flex; gap: 2px; pointer-events: none; }
88
- .media-grid__thumb { aspect-ratio: 1; width: 100%; object-fit: cover; border-radius: var(--radius-sm); background: var(--color-bg); display: block; }
89
- .media-grid__thumb--folder { display: grid; place-items: center; color: var(--color-text-subtle); }
90
- .media-grid__badge { display: grid; place-items: center; aspect-ratio: 1; width: 100%; border-radius: var(--radius-sm); background: var(--color-bg); color: var(--color-text-muted); font-weight: var(--weight-bold); }
91
- // Hover/focus reveal of the dimensions · size caption, overlaid on the lower edge of the thumb.
92
- .media-grid__meta {
93
- position: absolute;
94
- inset-inline: 0;
95
- bottom: 0;
96
- padding: 2px var(--space-1);
97
- font-size: var(--text-xs);
98
- line-height: 1.35;
99
- text-align: center;
100
- white-space: nowrap;
101
- overflow: hidden;
102
- text-overflow: ellipsis;
103
- color: var(--color-on-primary);
104
- background: linear-gradient(transparent, var(--color-scrim));
105
- border-radius: 0 0 var(--radius-sm) var(--radius-sm);
106
- opacity: 0;
107
- transition: opacity var(--motion-fast) var(--ease-standard);
108
- pointer-events: none;
109
- }
110
- .media-grid__tile:hover .media-grid__meta,
111
- .media-grid__tile:focus-visible .media-grid__meta { opacity: 1; }
112
- .media-grid__name { font-size: var(--text-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
113
- </style>
@@ -1,70 +0,0 @@
1
- <script setup lang="ts">
2
- import { computed } from 'vue'
3
- import { useBlurUp } from '../composables/useBlurUp'
4
-
5
- interface ResolvedMedia {
6
- alt: string | null
7
- width: number | null
8
- height: number | null
9
- thumbhash: string | null
10
- src: string
11
- srcset: { url: string; width: number }[]
12
- }
13
- // `sizes` lets the consuming layout declare the rendered slot width (e.g.
14
- // `(min-width: 768px) 50vw, 100vw`) so the browser picks an appropriately small
15
- // `srcset` candidate instead of defaulting to 100vw and over-fetching the largest.
16
- // `priority` marks the above-the-fold LCP image: load it eagerly with a high fetch priority instead of
17
- // lazily, so the browser fetches it immediately rather than deferring the most important pixel. Opt-in
18
- // per call site (the editor can't know which block is the LCP one); leave it off for everything else.
19
- const props = defineProps<{ media: ResolvedMedia; sizes?: string; priority?: boolean }>()
20
- // Computed, not a captured const: the live preview REUSES this instance (BlockRenderer keys blocks by id,
21
- // consumer carousels key slides by index), so a media reorder/replace only patches the `media` prop — a
22
- // one-shot const would keep the browser painting the OLD image via its stale `w`-descriptor candidate set.
23
- const srcset = computed(() => props.media.srcset.map((s) => `${s.url} ${s.width}w`).join(', ') || undefined)
24
-
25
- // Blur-up: a thumbhash placeholder shows until the raster paints, then a one-shot "sharpen-in" resolves
26
- // the blur into the crisp image (see useBlurUp for the SSR/no-JS/cached-image degradation rules). The
27
- // placeholder is painted as the <img>'s own background, so no wrapper element is introduced — the bare
28
- // <img> keeps the public site's `img` box model (the reset's display:block / max-width:100%) intact.
29
- const { imgEl, animate, placeholderStyle, onLoad } = useBlurUp(() => props.media.thumbhash)
30
- </script>
31
-
32
- <template>
33
- <img
34
- ref="imgEl"
35
- :src="media.src"
36
- :srcset="srcset"
37
- :sizes="sizes"
38
- :width="media.width ?? undefined"
39
- :height="media.height ?? undefined"
40
- :alt="media.alt ?? ''"
41
- :class="{ 'media-image--in': animate }"
42
- :style="placeholderStyle"
43
- :loading="priority ? 'eager' : 'lazy'"
44
- :fetchpriority="priority ? 'high' : undefined"
45
- decoding="async"
46
- @load="onLoad"
47
- >
48
- </template>
49
-
50
- <style scoped>
51
- /* Sharpen-in: when the real raster finishes loading it briefly starts soft and resolves to crisp, so the
52
- swap from the thumbhash placeholder reads as a smooth focus pull rather than a hard snap. The class is
53
- applied by JS only on a genuine async load, so SSR / no-JS / cached loads just show the image. */
54
- @keyframes media-sharpen {
55
- from {
56
- filter: blur(8px);
57
- }
58
- to {
59
- filter: blur(0);
60
- }
61
- }
62
- .media-image--in {
63
- animation: media-sharpen 0.5s ease-out;
64
- }
65
- @media (prefers-reduced-motion: reduce) {
66
- .media-image--in {
67
- animation: none;
68
- }
69
- }
70
- </style>