@michaelthielemann/kestrel 4.1.3 → 5.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (342) hide show
  1. package/README.md +26 -201
  2. package/dist/boot.d.ts +45 -0
  3. package/dist/boot.js +284 -0
  4. package/dist/cast.d.ts +3 -0
  5. package/dist/cast.js +6 -0
  6. package/dist/catalogue.d.ts +11 -0
  7. package/dist/catalogue.js +1 -0
  8. package/dist/cli.d.ts +2 -0
  9. package/dist/cli.js +55 -0
  10. package/dist/context.d.ts +62 -0
  11. package/dist/context.js +61 -0
  12. package/dist/dataflow.d.ts +3 -0
  13. package/dist/dataflow.js +65 -0
  14. package/dist/defineConfig.d.ts +162 -0
  15. package/dist/defineConfig.js +42 -0
  16. package/dist/defineContract.d.ts +12 -0
  17. package/dist/defineContract.js +16 -0
  18. package/dist/defineModule.d.ts +75 -0
  19. package/dist/defineModule.js +9 -0
  20. package/dist/definePipeline.d.ts +7 -0
  21. package/dist/definePipeline.js +11 -0
  22. package/dist/errors.d.ts +26 -0
  23. package/dist/errors.js +48 -0
  24. package/dist/index.d.ts +19 -0
  25. package/dist/index.js +14 -0
  26. package/dist/load.d.ts +7 -0
  27. package/dist/load.js +79 -0
  28. package/dist/logger.d.ts +18 -0
  29. package/dist/logger.js +36 -0
  30. package/dist/registry.d.ts +14 -0
  31. package/dist/registry.js +87 -0
  32. package/dist/result.d.ts +19 -0
  33. package/dist/result.js +19 -0
  34. package/dist/runner.d.ts +29 -0
  35. package/dist/runner.js +133 -0
  36. package/dist/schema.d.ts +7 -0
  37. package/dist/schema.js +142 -0
  38. package/dist/sort.d.ts +2 -0
  39. package/dist/sort.js +50 -0
  40. package/dist/testing/runPipeline.d.ts +9 -0
  41. package/dist/testing/runPipeline.js +22 -0
  42. package/dist/triggers/allowlist.d.ts +9 -0
  43. package/dist/triggers/allowlist.js +49 -0
  44. package/dist/triggers/cron.d.ts +10 -0
  45. package/dist/triggers/cron.js +44 -0
  46. package/dist/triggers/cronMatch.d.ts +10 -0
  47. package/dist/triggers/cronMatch.js +68 -0
  48. package/dist/triggers/http.d.ts +59 -0
  49. package/dist/triggers/http.js +300 -0
  50. package/package.json +25 -151
  51. package/NOTICE +0 -12
  52. package/kestrel.config.ts +0 -28
  53. package/layers/access/nuxt.config.ts +0 -5
  54. package/layers/access/server/middleware/00.ip-allowlist.ts +0 -52
  55. package/layers/access/server/middleware/access-guard.ts +0 -32
  56. package/layers/access/server/types.d.ts +0 -7
  57. package/layers/admin/app/components/AdminAccount.vue +0 -212
  58. package/layers/admin/app/components/AdminNav.vue +0 -44
  59. package/layers/admin/app/components/BlockFields.vue +0 -67
  60. package/layers/admin/app/components/BlockPreview.vue +0 -413
  61. package/layers/admin/app/components/BlockTree.vue +0 -433
  62. package/layers/admin/app/components/BlocksBody.vue +0 -163
  63. package/layers/admin/app/components/CollectionDeleteDialog.vue +0 -38
  64. package/layers/admin/app/components/CollectionEditor.vue +0 -324
  65. package/layers/admin/app/components/CollectionList.vue +0 -429
  66. package/layers/admin/app/components/CollectionListBulkBar.vue +0 -62
  67. package/layers/admin/app/components/CollectionListColumnsPanel.vue +0 -37
  68. package/layers/admin/app/components/CollectionListFilterPanel.vue +0 -145
  69. package/layers/admin/app/components/CollectionListPager.vue +0 -70
  70. package/layers/admin/app/components/CollectionListTable.vue +0 -347
  71. package/layers/admin/app/components/EditorStatus.vue +0 -211
  72. package/layers/admin/app/components/EditorUnsupported.vue +0 -28
  73. package/layers/admin/app/components/FieldsBody.vue +0 -23
  74. package/layers/admin/app/components/LocaleBar.vue +0 -107
  75. package/layers/admin/app/components/PageFields.vue +0 -129
  76. package/layers/admin/app/components/PageFieldsPane.vue +0 -39
  77. package/layers/admin/app/components/SeoFields.vue +0 -210
  78. package/layers/admin/app/components/SingletonEditor.vue +0 -93
  79. package/layers/admin/app/composables/useAuth.ts +0 -68
  80. package/layers/admin/app/composables/useBlockTree.ts +0 -95
  81. package/layers/admin/app/composables/useBlocks.ts +0 -7
  82. package/layers/admin/app/composables/useCachedResource.ts +0 -35
  83. package/layers/admin/app/composables/useCollectionOps.ts +0 -102
  84. package/layers/admin/app/composables/useCollections.ts +0 -6
  85. package/layers/admin/app/composables/useContentLocales.ts +0 -23
  86. package/layers/admin/app/composables/useEditForm.ts +0 -402
  87. package/layers/admin/app/composables/useLinkResolver.ts +0 -40
  88. package/layers/admin/app/composables/useListBatchActions.ts +0 -78
  89. package/layers/admin/app/composables/useListColumns.ts +0 -53
  90. package/layers/admin/app/composables/useListFilterDraft.ts +0 -129
  91. package/layers/admin/app/composables/useListRows.ts +0 -75
  92. package/layers/admin/app/composables/useListSelection.ts +0 -28
  93. package/layers/admin/app/composables/useListUrlState.ts +0 -53
  94. package/layers/admin/app/composables/usePendingPublishPoll.ts +0 -15
  95. package/layers/admin/app/composables/usePreviewViewport.ts +0 -30
  96. package/layers/admin/app/composables/usePublishStatus.ts +0 -107
  97. package/layers/admin/app/composables/useRailCollapsed.ts +0 -14
  98. package/layers/admin/app/composables/useTheme.ts +0 -36
  99. package/layers/admin/app/composables/useToolbarPanel.ts +0 -38
  100. package/layers/admin/app/composables/useUnsavedGuard.ts +0 -22
  101. package/layers/admin/app/layouts/admin.vue +0 -266
  102. package/layers/admin/app/middleware/admin-auth.ts +0 -4
  103. package/layers/admin/app/pages/admin/[collection]/[id].vue +0 -203
  104. package/layers/admin/app/pages/admin/[collection]/index.vue +0 -62
  105. package/layers/admin/app/pages/admin/index.vue +0 -118
  106. package/layers/admin/app/pages/admin/login.vue +0 -68
  107. package/layers/admin/app/pages/admin/media.vue +0 -28
  108. package/layers/admin/app/pages/admin/references.vue +0 -146
  109. package/layers/admin/app/plugins/admin-style-guard.client.ts +0 -76
  110. package/layers/admin/app/plugins/reauth.client.ts +0 -20
  111. package/layers/admin/app/utils/admin-style-guard.ts +0 -20
  112. package/layers/admin/app/utils/block-tree.ts +0 -214
  113. package/layers/admin/app/utils/collection-ops.ts +0 -69
  114. package/layers/admin/app/utils/dead-refs.ts +0 -30
  115. package/layers/admin/app/utils/edit-form.ts +0 -267
  116. package/layers/admin/app/utils/editor-expose.ts +0 -36
  117. package/layers/admin/app/utils/editor-form-context.ts +0 -82
  118. package/layers/admin/app/utils/editor-registry.ts +0 -20
  119. package/layers/admin/app/utils/field-keys.ts +0 -11
  120. package/layers/admin/app/utils/list-cell.ts +0 -46
  121. package/layers/admin/app/utils/list-columns.ts +0 -142
  122. package/layers/admin/app/utils/list-query.ts +0 -96
  123. package/layers/admin/app/utils/nav-active.ts +0 -9
  124. package/layers/admin/app/utils/populate-blocks.ts +0 -288
  125. package/layers/admin/app/utils/preview-channel.ts +0 -62
  126. package/layers/admin/app/utils/preview-viewport.ts +0 -80
  127. package/layers/admin/app/utils/reauth.ts +0 -67
  128. package/layers/admin/app/utils/record-title.ts +0 -15
  129. package/layers/admin/app/utils/register-builtin-editors.ts +0 -9
  130. package/layers/admin/app/utils/safe-redirect.ts +0 -12
  131. package/layers/admin/nuxt.config.ts +0 -20
  132. package/layers/auth/nuxt.config.ts +0 -1
  133. package/layers/auth/server/plugins/01.register-auth-pipelines.ts +0 -7
  134. package/layers/collections/nuxt.config.ts +0 -1
  135. package/layers/collections/server/plugins/01.register.ts +0 -29
  136. package/layers/collections/server/plugins/02.register-relation-populate.ts +0 -18
  137. package/layers/core/app/composables/layouts.ts +0 -5
  138. package/layers/core/app/utils/field-factories.ts +0 -129
  139. package/layers/core/app/utils/layouts.ts +0 -38
  140. package/layers/core/modules/auto-discovery/extract-block.ts +0 -179
  141. package/layers/core/modules/auto-discovery/index.ts +0 -112
  142. package/layers/core/modules/auto-discovery/package-registry.ts +0 -19
  143. package/layers/core/modules/auto-discovery/scan.ts +0 -170
  144. package/layers/core/modules/auto-discovery/virtual.d.ts +0 -20
  145. package/layers/core/modules/component-namespace/index.ts +0 -44
  146. package/layers/core/modules/component-namespace/shared.ts +0 -24
  147. package/layers/core/modules/kestrel/app-shell.ts +0 -55
  148. package/layers/core/modules/kestrel/auto-imports.ts +0 -44
  149. package/layers/core/modules/kestrel/index.ts +0 -144
  150. package/layers/core/modules/plugin-order/index.ts +0 -69
  151. package/layers/core/modules/plugin-order/plugin-order.ts +0 -273
  152. package/layers/core/nuxt.config.ts +0 -10
  153. package/layers/core/server/api/[...path].ts +0 -98
  154. package/layers/core/server/plugins/00.config.ts +0 -12
  155. package/layers/core/server/plugins/00.migrate.ts +0 -12
  156. package/layers/core/server/plugins/01.register-introspection-pipeline.ts +0 -5
  157. package/layers/core/server/plugins/01.register-openapi-pipeline.ts +0 -5
  158. package/layers/core/server/plugins/01.register-tooling-pipelines.ts +0 -5
  159. package/layers/core/server/plugins/02.schema-sync.ts +0 -58
  160. package/layers/core/server/plugins/04.outbox-worker.ts +0 -31
  161. package/layers/core/server/plugins/05.reindex-refs.ts +0 -5
  162. package/layers/core/server/routes/__kestrel/dashboard.get.ts +0 -29
  163. package/layers/core/server/tasks/db/migrate-module.ts +0 -83
  164. package/layers/core/server/tasks/db/migrate-revisions.ts +0 -20
  165. package/layers/core/server/tasks/db/migrate.ts +0 -50
  166. package/layers/core/server/utils/server-config.ts +0 -81
  167. package/layers/fields/nuxt.config.ts +0 -1
  168. package/layers/fields/server/plugins/01.register-blocks-pipeline.ts +0 -7
  169. package/layers/fields/server/plugins/01.register-field-populate.ts +0 -11
  170. package/layers/media/app/components/KestrelImg.vue +0 -94
  171. package/layers/media/app/components/MediaConflictDialog.vue +0 -35
  172. package/layers/media/app/components/MediaDeleteDialog.vue +0 -52
  173. package/layers/media/app/components/MediaGrid.vue +0 -113
  174. package/layers/media/app/components/MediaImage.vue +0 -70
  175. package/layers/media/app/components/MediaLibrary.vue +0 -456
  176. package/layers/media/app/components/MediaNewFolderDialog.vue +0 -32
  177. package/layers/media/app/components/MediaPathBar.vue +0 -163
  178. package/layers/media/app/components/MediaPicker.vue +0 -19
  179. package/layers/media/app/components/MediaRenameDialog.vue +0 -32
  180. package/layers/media/app/components/MediaTable.vue +0 -75
  181. package/layers/media/app/components/MediaThumb.vue +0 -25
  182. package/layers/media/app/components/MediaToolbar.vue +0 -90
  183. package/layers/media/app/components/MediaUploadDialog.vue +0 -87
  184. package/layers/media/app/components/MediaViewer.vue +0 -137
  185. package/layers/media/app/components/field/Media.vue +0 -263
  186. package/layers/media/app/composables/useBlurUp.ts +0 -55
  187. package/layers/media/app/composables/useMediaClipboard.ts +0 -14
  188. package/layers/media/app/composables/useMediaContextMenu.ts +0 -56
  189. package/layers/media/app/composables/useMediaDnd.ts +0 -56
  190. package/layers/media/app/composables/useMediaLibrary.ts +0 -152
  191. package/layers/media/app/composables/useMediaOps.ts +0 -50
  192. package/layers/media/app/composables/useMediaRelocate.ts +0 -39
  193. package/layers/media/app/composables/useMediaResolver.ts +0 -50
  194. package/layers/media/app/composables/useMediaUpload.ts +0 -93
  195. package/layers/media/app/composables/useMediaVariant.ts +0 -34
  196. package/layers/media/app/plugins/field-media.client.ts +0 -6
  197. package/layers/media/app/utils/ai-disclosure.ts +0 -19
  198. package/layers/media/app/utils/dnd.ts +0 -69
  199. package/layers/media/app/utils/library.ts +0 -81
  200. package/layers/media/app/utils/ops.ts +0 -60
  201. package/layers/media/app/utils/picture-model.ts +0 -125
  202. package/layers/media/app/utils/thumbhash.ts +0 -19
  203. package/layers/media/app/utils/variant-name.ts +0 -20
  204. package/layers/media/nuxt.config.ts +0 -7
  205. package/layers/media/server/middleware/ondemand-variants.ts +0 -38
  206. package/layers/media/server/plugins/01.register-media-pipelines.ts +0 -7
  207. package/layers/media/server/plugins/02.register-media.ts +0 -22
  208. package/layers/media/server/plugins/04.variant-capture.ts +0 -27
  209. package/layers/media/server/plugins/05.media-cleanup.ts +0 -5
  210. package/layers/media/server/tasks/media/backfill.ts +0 -17
  211. package/layers/public/app/app.vue +0 -5
  212. package/layers/public/app/components/BlockRenderer.vue +0 -56
  213. package/layers/public/app/components/KestrelLink.vue +0 -26
  214. package/layers/public/app/components/KestrelPreviewBridge.vue +0 -133
  215. package/layers/public/app/composables/public-page.ts +0 -24
  216. package/layers/public/app/error.vue +0 -12
  217. package/layers/public/app/layouts/default.vue +0 -5
  218. package/layers/public/app/pages/[...slug].vue +0 -239
  219. package/layers/public/app/pages/__kestrel/preview.vue +0 -46
  220. package/layers/public/app/utils/block-edit-context.ts +0 -16
  221. package/layers/public/app/utils/json-ld.ts +0 -139
  222. package/layers/public/app/utils/link-href.ts +0 -46
  223. package/layers/public/app/utils/page-head.ts +0 -84
  224. package/layers/public/app/utils/page-layout.ts +0 -18
  225. package/layers/public/app/utils/preview-protocol.ts +0 -117
  226. package/layers/public/app/utils/public-route.ts +0 -46
  227. package/layers/public/app/utils/site-head.ts +0 -43
  228. package/layers/public/modules/deploy-output/index.ts +0 -80
  229. package/layers/public/modules/prerender-routes/discover.ts +0 -37
  230. package/layers/public/modules/prerender-routes/index.ts +0 -88
  231. package/layers/public/modules/prerender-routes/routes.ts +0 -16
  232. package/layers/public/modules/prune-media/index.ts +0 -48
  233. package/layers/public/modules/prune-media/prune-media.ts +0 -81
  234. package/layers/public/nuxt.config.ts +0 -14
  235. package/layers/public/server/delivery-live/serve.ts +0 -111
  236. package/layers/public/server/middleware/delivery-live-catchall.ts +0 -7
  237. package/layers/public/server/pipelines/index.ts +0 -23
  238. package/layers/public/server/plugins/00.ensure-snapshot-triggers.ts +0 -14
  239. package/layers/public/server/plugins/01.register-public-pipelines.ts +0 -7
  240. package/layers/public/server/plugins/02.register-links.ts +0 -16
  241. package/layers/public/server/plugins/03.redirects.ts +0 -55
  242. package/layers/public/server/plugins/05.plan-publish.ts +0 -5
  243. package/layers/public/server/plugins/zz.publish.ts +0 -127
  244. package/layers/public/server/routes/llms-full.txt.get.ts +0 -99
  245. package/layers/public/server/routes/llms.txt.get.ts +0 -66
  246. package/layers/public/server/routes/redirects.json.get.ts +0 -58
  247. package/layers/public/server/routes/robots.txt.get.ts +0 -11
  248. package/layers/public/server/routes/sitemap.xml.get.ts +0 -79
  249. package/layers/public/server/tasks/publish/run.ts +0 -32
  250. package/layers/public/server/utils/publish/render-live.ts +0 -26
  251. package/layers/ui/app/assets/scss/_base.scss +0 -16
  252. package/layers/ui/app/assets/scss/_datepicker.scss +0 -82
  253. package/layers/ui/app/assets/scss/_mixins.scss +0 -50
  254. package/layers/ui/app/assets/scss/_reset.scss +0 -65
  255. package/layers/ui/app/assets/scss/_tokens.scss +0 -183
  256. package/layers/ui/app/assets/scss/main.scss +0 -4
  257. package/layers/ui/app/components/field/Boolean.vue +0 -30
  258. package/layers/ui/app/components/field/Choice.vue +0 -73
  259. package/layers/ui/app/components/field/Datetime.vue +0 -98
  260. package/layers/ui/app/components/field/DeadRefNote.vue +0 -28
  261. package/layers/ui/app/components/field/Json.vue +0 -62
  262. package/layers/ui/app/components/field/Layout.vue +0 -138
  263. package/layers/ui/app/components/field/Link.vue +0 -112
  264. package/layers/ui/app/components/field/LinkInternalPicker.vue +0 -95
  265. package/layers/ui/app/components/field/Number.vue +0 -30
  266. package/layers/ui/app/components/field/Relation.vue +0 -49
  267. package/layers/ui/app/components/field/Renderer.vue +0 -30
  268. package/layers/ui/app/components/field/Repeater.vue +0 -460
  269. package/layers/ui/app/components/field/Richtext.vue +0 -26
  270. package/layers/ui/app/components/field/Slug.vue +0 -65
  271. package/layers/ui/app/components/field/Text.vue +0 -35
  272. package/layers/ui/app/components/field/Unsupported.vue +0 -34
  273. package/layers/ui/app/components/ui/Alert.vue +0 -50
  274. package/layers/ui/app/components/ui/Brand.vue +0 -22
  275. package/layers/ui/app/components/ui/Button.vue +0 -116
  276. package/layers/ui/app/components/ui/ButtonGroup.vue +0 -96
  277. package/layers/ui/app/components/ui/Checkbox.vue +0 -53
  278. package/layers/ui/app/components/ui/CheckboxGroup.vue +0 -51
  279. package/layers/ui/app/components/ui/Combobox.vue +0 -345
  280. package/layers/ui/app/components/ui/DatePicker.vue +0 -92
  281. package/layers/ui/app/components/ui/DateRangePicker.vue +0 -100
  282. package/layers/ui/app/components/ui/Dialog.vue +0 -107
  283. package/layers/ui/app/components/ui/EmptyState.vue +0 -46
  284. package/layers/ui/app/components/ui/Field.vue +0 -58
  285. package/layers/ui/app/components/ui/Fieldset.vue +0 -65
  286. package/layers/ui/app/components/ui/Icon.vue +0 -44
  287. package/layers/ui/app/components/ui/Menu.vue +0 -54
  288. package/layers/ui/app/components/ui/NumberInput.vue +0 -175
  289. package/layers/ui/app/components/ui/Richtext.vue +0 -141
  290. package/layers/ui/app/components/ui/RichtextToolbar.vue +0 -251
  291. package/layers/ui/app/components/ui/Select.vue +0 -52
  292. package/layers/ui/app/components/ui/TextInput.vue +0 -111
  293. package/layers/ui/app/components/ui/Textarea.vue +0 -39
  294. package/layers/ui/app/components/ui/TimeInput.vue +0 -18
  295. package/layers/ui/app/components/ui/Toasts.vue +0 -103
  296. package/layers/ui/app/components/ui/Tooltip.vue +0 -45
  297. package/layers/ui/app/composables/useAdminLang.ts +0 -8
  298. package/layers/ui/app/composables/useDragReorder.ts +0 -41
  299. package/layers/ui/app/composables/useEchoGuard.ts +0 -28
  300. package/layers/ui/app/composables/useLinkField.ts +0 -70
  301. package/layers/ui/app/composables/useRecordOptions.ts +0 -84
  302. package/layers/ui/app/composables/useRepeater.ts +0 -99
  303. package/layers/ui/app/composables/useT.ts +0 -32
  304. package/layers/ui/app/composables/useToast.ts +0 -46
  305. package/layers/ui/app/i18n/de.ts +0 -432
  306. package/layers/ui/app/i18n/en.ts +0 -434
  307. package/layers/ui/app/utils/boolean-field.ts +0 -10
  308. package/layers/ui/app/utils/field-component.ts +0 -22
  309. package/layers/ui/app/utils/field-empty.ts +0 -21
  310. package/layers/ui/app/utils/field-registry.ts +0 -36
  311. package/layers/ui/app/utils/humanize.ts +0 -14
  312. package/layers/ui/app/utils/icons.ts +0 -143
  313. package/layers/ui/app/utils/iso-date.ts +0 -18
  314. package/layers/ui/app/utils/localized.ts +0 -13
  315. package/layers/ui/app/utils/record-context.ts +0 -15
  316. package/layers/ui/app/utils/reorder.ts +0 -8
  317. package/layers/ui/app/utils/richtext-preserve-class.ts +0 -31
  318. package/layers/ui/app/utils/useFieldA11y.ts +0 -40
  319. package/layers/ui/nuxt.config.ts +0 -9
  320. package/nuxt.config.ts +0 -108
  321. package/scripts/consumer-template-ci.mjs +0 -160
  322. package/scripts/copy-create-payload.mjs +0 -49
  323. package/scripts/coverage-all.mjs +0 -163
  324. package/scripts/dashboard.mjs +0 -89
  325. package/scripts/hash-password.mjs +0 -29
  326. package/scripts/kestrel.mjs +0 -240
  327. package/scripts/lib/cli.mjs +0 -129
  328. package/scripts/lib/coverage-model.mjs +0 -51
  329. package/scripts/lib/password.mjs +0 -19
  330. package/scripts/lib/scaffold.mjs +0 -205
  331. package/scripts/port-graph.mjs +0 -350
  332. package/scripts/typecheck.mjs +0 -62
  333. package/templates/starter/README.md +0 -65
  334. package/templates/starter/_env.example +0 -17
  335. package/templates/starter/_gitignore +0 -26
  336. package/templates/starter/_package.json +0 -22
  337. package/templates/starter/app/app.vue +0 -7
  338. package/templates/starter/app/blocks/Prose.vue +0 -13
  339. package/templates/starter/app/layouts/default.vue +0 -6
  340. package/templates/starter/nuxt.config.ts +0 -23
  341. package/templates/starter/pnpm-workspace.yaml +0 -8
  342. package/templates/starter/tsconfig.json +0 -3
@@ -1,433 +0,0 @@
1
- <script setup lang="ts">
2
- import { ref } from 'vue'
3
- import type { BlockRow, BlockTreeCtx } from '../utils/block-tree'
4
- import { resolveLocalized } from '../../../ui/app/utils/localized'
5
- import BlockTree from './BlockTree.vue'
6
-
7
- const props = withDefaults(
8
- defineProps<{
9
- blocks: BlockRow[]
10
- selectedId: string | null
11
- errorIds: Set<string>
12
- /** Block ids that hold (or contain) a stale reference — a warning roll-up, parallel to `errorIds`. */
13
- deadRefIds?: Set<string>
14
- ctx: BlockTreeCtx
15
- disabled?: boolean
16
- /** Set on the parent block + slot when this list is a slot's children; null/null at the page root. */
17
- parentId?: string | null
18
- slotName?: string | null
19
- /** Only the top-level instance renders the selectable "Page" root node. */
20
- root?: boolean
21
- }>(),
22
- { deadRefIds: () => new Set<string>() },
23
- )
24
-
25
- const { t, lang } = useT()
26
-
27
- const slotNamesOf = (type: string): string[] => props.ctx.byName[type]?.slots ?? []
28
- const labelOf = (block: BlockRow): string => resolveLocalized(props.ctx.byName[block.type]?.label, lang.value) ?? block.type
29
-
30
- // Inline "Add block" type picker. The block type is fixed at creation (not switchable afterwards), so
31
- // the only type choice happens here. State is per tree instance, so the root list and each slot keep
32
- // independent pickers. Inline (not teleported) keeps it directly DOM-testable.
33
- const picking = ref(false)
34
- function pick(type: string): void {
35
- props.ctx.ops.add(props.parentId ?? null, props.slotName ?? null, type)
36
- picking.value = false
37
- }
38
- </script>
39
-
40
- <template>
41
- <div class="block-tree">
42
- <button
43
- v-if="root"
44
- type="button"
45
- class="block-tree__root"
46
- :class="{ 'block-tree__node-label--selected': selectedId === null }"
47
- :aria-pressed="selectedId === null"
48
- @click="ctx.ops.select(null)"
49
- >
50
- <KestrelUiIcon name="file-text" :size="15" class="block-tree__root-icon" />
51
- <span>{{ t('blocks.page') }}</span>
52
- </button>
53
-
54
- <p v-if="root && !blocks.length" class="block-tree__empty">{{ t('blocks.empty') }}</p>
55
-
56
- <ul class="block-tree__list">
57
- <li
58
- v-for="(block, i) in blocks"
59
- :key="block.id"
60
- class="block-tree__node"
61
- :class="{ 'block-tree__node--selected': selectedId === block.id }"
62
- :data-type="block.type"
63
- >
64
- <div class="block-tree__row">
65
- <button
66
- type="button"
67
- class="block-tree__node-label"
68
- :class="{ 'block-tree__node-label--selected': selectedId === block.id }"
69
- :aria-pressed="selectedId === block.id"
70
- @click="ctx.ops.select(block.id)"
71
- >
72
- <span class="block-tree__node-name">{{ labelOf(block) }}</span>
73
- <span v-if="errorIds.has(block.id)" class="block-tree__badge" role="img" :aria-label="t('blocks.invalid')" :title="t('blocks.invalid')">!</span>
74
- <KestrelUiIcon v-if="deadRefIds.has(block.id)" name="triangle-alert" :size="14" class="block-tree__deadbadge" role="img" :aria-label="t('deadRefs.blockBadge')" :title="t('deadRefs.blockBadge')" />
75
- </button>
76
- <div class="block-tree__actions">
77
- <button type="button" class="block-tree__btn" :disabled="disabled || i === 0" :aria-label="t('blocks.moveUp', { n: i + 1 })" @click="ctx.ops.move(block.id, -1)"><KestrelUiIcon name="chevron-up" :size="15" /></button>
78
- <button type="button" class="block-tree__btn" :disabled="disabled || i === blocks.length - 1" :aria-label="t('blocks.moveDown', { n: i + 1 })" @click="ctx.ops.move(block.id, 1)"><KestrelUiIcon name="chevron-down" :size="15" /></button>
79
- <button type="button" class="block-tree__btn" :disabled="disabled" :aria-label="t('blocks.duplicate', { n: i + 1 })" @click="ctx.ops.duplicate(block.id)"><KestrelUiIcon name="copy" :size="15" /></button>
80
- <button type="button" class="block-tree__btn block-tree__btn--danger" :disabled="disabled" :aria-label="t('blocks.remove', { n: i + 1 })" @click="ctx.ops.remove(block.id)"><KestrelUiIcon name="trash" :size="15" /></button>
81
- </div>
82
- </div>
83
-
84
- <div v-if="slotNamesOf(block.type).length" class="block-tree__slots">
85
- <div v-for="name in slotNamesOf(block.type)" :key="name" class="block-tree__slot">
86
- <span class="block-tree__slot-label">{{ name }}</span>
87
- <BlockTree
88
- :blocks="(block.slots?.[name] as BlockRow[]) ?? []"
89
- :selected-id="selectedId"
90
- :error-ids="errorIds"
91
- :dead-ref-ids="deadRefIds"
92
- :ctx="ctx"
93
- :disabled="disabled"
94
- :parent-id="block.id"
95
- :slot-name="name"
96
- />
97
- </div>
98
- </div>
99
- </li>
100
- </ul>
101
-
102
- <div class="block-tree__add">
103
- <button
104
- type="button"
105
- class="block-tree__add-btn"
106
- :class="{ 'block-tree__add-btn--active': picking }"
107
- :disabled="disabled"
108
- aria-haspopup="dialog"
109
- :aria-expanded="picking"
110
- @click="picking = true"
111
- >
112
- <KestrelUiIcon name="plus" :size="15" />
113
- <span>{{ slotName ? t('blocks.addInto', { slot: slotName }) : t('blocks.add') }}</span>
114
- </button>
115
-
116
- <!-- The type picker lives in a centered modal (position:fixed) so it escapes the tree pane's
117
- overflow-y:auto clip; reka's DialogRoot owns Escape/outside-click dismissal + focus restore. -->
118
- <KestrelUiDialog :open="picking" size="lg" :title="t('blocks.pickType')" @update:open="picking = $event">
119
- <!-- future: category tabs + a search input mount here, above the grid -->
120
- <ul class="block-tree__picker" :aria-label="t('blocks.pickType')">
121
- <li v-for="bt in ctx.allowedTypes" :key="bt.name">
122
- <button
123
- type="button"
124
- class="block-tree__picker-item"
125
- :disabled="disabled"
126
- :aria-label="t('blocks.addOfType', { type: resolveLocalized(bt.label, lang) ?? bt.name })"
127
- @click="pick(bt.name)"
128
- >
129
- <img v-if="bt.image" :src="bt.image" alt="" loading="lazy" class="block-tree__picker-img" />
130
- <KestrelUiIcon v-else :name="bt.icon ?? 'layout-grid'" :size="24" class="block-tree__picker-icon" />
131
- <span class="block-tree__picker-label">{{ resolveLocalized(bt.label, lang) ?? bt.name }}</span>
132
- </button>
133
- </li>
134
- </ul>
135
- </KestrelUiDialog>
136
- </div>
137
- </div>
138
- </template>
139
-
140
- <style lang="scss">
141
- .block-tree {
142
- display: flex;
143
- flex-direction: column;
144
- gap: var(--space-1);
145
- font-size: var(--text-sm);
146
-
147
- &__root,
148
- &__node-label {
149
- display: flex;
150
- align-items: center;
151
- gap: var(--space-2);
152
- flex: 1;
153
- min-width: 0;
154
- padding: var(--space-1) var(--space-2);
155
- border: 0;
156
- border-radius: var(--radius-sm);
157
- background: transparent;
158
- font: inherit;
159
- text-align: left;
160
- text-transform: capitalize;
161
- color: var(--color-text);
162
- cursor: pointer;
163
-
164
- &:hover {
165
- background: var(--color-hover);
166
- }
167
- &:focus-visible {
168
- outline: 2px solid var(--color-focus);
169
- outline-offset: -2px;
170
- }
171
- }
172
-
173
- // The page root reads as a quieter, structural header than the blocks below it.
174
- &__root {
175
- font-weight: var(--weight-medium);
176
- color: var(--color-text-muted);
177
- }
178
- &__root-icon {
179
- flex-shrink: 0;
180
- color: var(--color-text-subtle);
181
- }
182
-
183
- &__node-label--selected {
184
- background: var(--color-active, var(--color-surface-2));
185
- color: var(--color-text);
186
- font-weight: var(--weight-medium);
187
- box-shadow: inset 2px 0 0 var(--color-primary);
188
- }
189
-
190
- &__node-name {
191
- overflow: hidden;
192
- text-overflow: ellipsis;
193
- white-space: nowrap;
194
- }
195
-
196
- &__empty {
197
- margin: 0;
198
- padding: var(--space-1) var(--space-2);
199
- color: var(--color-text-muted);
200
- }
201
-
202
- &__list {
203
- list-style: none;
204
- margin: 0;
205
- padding: 0;
206
- display: flex;
207
- flex-direction: column;
208
- gap: 2px;
209
- }
210
- &__node {
211
- display: flex;
212
- flex-direction: column;
213
- gap: var(--space-1);
214
- }
215
- &__row {
216
- position: relative;
217
- display: flex;
218
- align-items: center;
219
- gap: var(--space-1);
220
- border-radius: var(--radius-sm);
221
- }
222
-
223
- &__badge {
224
- display: inline-flex;
225
- align-items: center;
226
- justify-content: center;
227
- flex-shrink: 0;
228
- width: 1rem;
229
- height: 1rem;
230
- border-radius: var(--radius-full);
231
- background: var(--color-danger-solid);
232
- color: var(--color-on-danger);
233
- font-size: var(--text-xs);
234
- font-weight: var(--weight-bold);
235
- line-height: 1;
236
- }
237
-
238
- // Stale-reference warning marker (distinct shape + colour from the red error badge).
239
- &__deadbadge {
240
- flex-shrink: 0;
241
- color: var(--color-warning);
242
- }
243
-
244
- // Structural controls stay quiet until the row is hovered, focused, or selected — the tree reads as
245
- // a calm outline, not a control panel. Overlaid (position:absolute) so the hidden bar reserves NO
246
- // width and the label spans the full row when idle. Kept in the DOM (revealed by :focus-within) so
247
- // they stay keyboard-reachable; the pointer-events toggle lets idle clicks fall through to the label.
248
- &__actions {
249
- position: absolute;
250
- top: 0;
251
- right: 0;
252
- bottom: 0;
253
- display: flex;
254
- align-items: center;
255
- gap: 1px;
256
- padding-left: var(--space-5);
257
- border-radius: var(--radius-sm);
258
- background: linear-gradient(to right, transparent, var(--color-surface) var(--space-4));
259
- opacity: 0;
260
- pointer-events: none;
261
- transition: opacity var(--motion-fast) var(--ease-standard);
262
- }
263
- &__row:hover &__actions,
264
- &__row:focus-within &__actions,
265
- &__node--selected > &__row &__actions {
266
- opacity: 1;
267
- pointer-events: auto;
268
- }
269
- // When the overlay is revealed, reserve room on the label so the name and the validity / stale-ref
270
- // badges aren't hidden under it (4×1.5rem buttons + a little clearance). Idle rows keep the full width.
271
- &__row:hover &__node-label,
272
- &__row:focus-within &__node-label,
273
- &__node--selected > &__row &__node-label {
274
- padding-right: 6.25rem;
275
- }
276
-
277
- &__btn {
278
- display: flex;
279
- align-items: center;
280
- justify-content: center;
281
- width: 1.5rem;
282
- height: 1.5rem;
283
- border: 0;
284
- border-radius: var(--radius-sm);
285
- background: transparent;
286
- color: var(--color-text-muted); /* meaningful icon control at rest → ≥3:1 (SC 1.4.11) */
287
- cursor: pointer;
288
- transition:
289
- background-color var(--motion-fast) var(--ease-standard),
290
- color var(--motion-fast) var(--ease-standard);
291
-
292
- &:hover:not(:disabled) {
293
- background: var(--color-hover);
294
- color: var(--color-text);
295
- }
296
- &:focus-visible {
297
- outline: 2px solid var(--color-focus);
298
- outline-offset: -2px;
299
- }
300
- &:disabled {
301
- opacity: 0.35;
302
- cursor: default;
303
- }
304
- &--danger:hover:not(:disabled) {
305
- color: var(--color-danger);
306
- }
307
- }
308
-
309
- &__slots {
310
- display: flex;
311
- flex-direction: column;
312
- gap: var(--space-1);
313
- // Subtle step (~13px/level) so deep nests don't eat the label width; the 1px guide keeps it legible.
314
- margin: 2px 0 var(--space-1) var(--space-1);
315
- padding-left: var(--space-2);
316
- border-left: 1px solid var(--color-border);
317
- }
318
- &__slot {
319
- display: flex;
320
- flex-direction: column;
321
- gap: 2px;
322
- }
323
- &__slot-label {
324
- padding: 0 var(--space-2);
325
- font-size: var(--text-xs);
326
- font-weight: var(--weight-medium);
327
- color: var(--color-text-muted);
328
- text-transform: capitalize;
329
- }
330
-
331
- &__add {
332
- position: relative;
333
- margin-top: 2px;
334
- }
335
- &__add-btn {
336
- display: flex;
337
- align-items: center;
338
- gap: var(--space-2);
339
- width: 100%;
340
- padding: var(--space-1) var(--space-2);
341
- border: 0;
342
- border-radius: var(--radius-sm);
343
- background: transparent;
344
- color: var(--color-text-muted);
345
- font: inherit;
346
- font-size: var(--text-sm);
347
- text-align: left;
348
- cursor: pointer;
349
- transition:
350
- background-color var(--motion-fast) var(--ease-standard),
351
- color var(--motion-fast) var(--ease-standard);
352
-
353
- &:hover:not(:disabled) {
354
- background: var(--color-hover);
355
- color: var(--color-text);
356
- }
357
- &:focus-visible {
358
- outline: 2px solid var(--color-focus);
359
- outline-offset: -2px;
360
- }
361
- &:disabled {
362
- opacity: 0.5;
363
- cursor: default;
364
- }
365
- }
366
- // While its picker modal is open, the trigger carries a high-contrast solid-indigo fill so it reads as
367
- // the active insertion target (UiIcon uses currentColor, so the + glyph flips to on-primary too).
368
- &__add-btn--active,
369
- &__add-btn--active:hover:not(:disabled) {
370
- background: var(--color-primary-solid);
371
- color: var(--color-on-primary);
372
- }
373
-
374
- // Tile grid inside the picker dialog body (the dialog provides the card chrome). Structured so a future
375
- // category-tab row + search input can mount above it. Tiles widen to hold optional preview screenshots.
376
- &__picker {
377
- list-style: none;
378
- margin: 0;
379
- padding: 0;
380
- display: grid;
381
- grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr));
382
- gap: var(--space-2);
383
- }
384
- &__picker-item {
385
- display: flex;
386
- flex-direction: column;
387
- align-items: center;
388
- justify-content: center;
389
- gap: var(--space-2);
390
- width: 100%;
391
- padding: var(--space-2);
392
- border: 1px solid transparent;
393
- border-radius: var(--radius-sm);
394
- background: transparent;
395
- color: var(--color-text);
396
- font: inherit;
397
- font-size: var(--text-xs);
398
- line-height: 1.2;
399
- text-align: center;
400
- text-transform: capitalize;
401
- cursor: pointer;
402
-
403
- &:hover:not(:disabled) {
404
- background: var(--color-hover);
405
- border-color: var(--color-border);
406
- }
407
- &:focus-visible {
408
- outline: 2px solid var(--color-focus);
409
- outline-offset: -2px;
410
- }
411
- &:disabled {
412
- opacity: 0.5;
413
- cursor: default;
414
- }
415
- }
416
- &__picker-icon {
417
- color: var(--color-text-subtle);
418
- }
419
- &__picker-img {
420
- width: 100%;
421
- aspect-ratio: 16 / 10;
422
- object-fit: cover;
423
- border: 1px solid var(--color-border);
424
- border-radius: var(--radius-sm);
425
- }
426
- &__picker-label {
427
- display: block;
428
- max-width: 100%;
429
- overflow: hidden;
430
- text-overflow: ellipsis;
431
- }
432
- }
433
- </style>
@@ -1,163 +0,0 @@
1
- <script setup lang="ts">
2
- import type { SerializedBlock } from '@michaelthielemann/kestrel-core'
3
- import type { BlockTreeCtx } from '../utils/block-tree'
4
-
5
- // The page-builder editor body: hierarchy · live preview · contextual fields. All form state comes from
6
- // the editor form context (CollectionEditor); the block-specific machinery lives here.
7
- const { t } = useT()
8
- const ctx = useEditorFormContext()
9
- const { values, setField, blockErrors, deadRefs, saving, locale, blocksAllowed } = ctx
10
-
11
- // Block registry. This body mounts only for a blocks-enabled collection, so the load always runs.
12
- const { load } = useBlocks()
13
- const all = await load()
14
- const byName = computed<Record<string, SerializedBlock>>(() => Object.fromEntries(all.map((b) => [b.name, b])))
15
- const allowedTypes = computed(() => (blocksAllowed.value?.length ? all.filter((b) => blocksAllowed.value!.includes(b.name)) : all))
16
-
17
- // One writable view of `content` so every tree mutation routes back through `setField` (→ block-error
18
- // reconciliation).
19
- const content = computed<unknown[]>({
20
- get: () => (values.content as unknown[]) ?? [],
21
- set: (v) => setField('content', v),
22
- })
23
- const tree = useBlockTree(content, byName, undefined, (v, coalesceAs) => setField('content', v, coalesceAs))
24
- const { selectedId, selectedBlock } = tree
25
-
26
- // Tree badges: a node is flagged if it (or any descendant, by roll-up) has an error.
27
- const errorIds = computed(() => errorBearingIds(tree.blocks.value, new Set(Object.keys(blockErrors.value))))
28
- // Stale-reference warnings (derived on read): the same ancestor roll-up as errors, plus the per-field set
29
- // for the selected block. (The page-root dead fields live on the shell's shared page-fields bindings.)
30
- const deadRefIds = computed(() => errorBearingIds(tree.blocks.value, deadBlockIds(deadRefs.value)))
31
- const blockDeadFields = computed(() => deadFieldsAt(deadRefs.value, selectedId.value))
32
-
33
- // Stable config + ops threaded through the recursive tree.
34
- const treeCtx: BlockTreeCtx = {
35
- byName: byName.value,
36
- allowedTypes: allowedTypes.value,
37
- ops: { select: tree.select, add: tree.add, remove: tree.remove, move: tree.move, duplicate: tree.duplicate },
38
- }
39
-
40
- // On a failed save: jump to the first offending block so its fields are in view; if none, deselect so the
41
- // page-fields pane remounts and its inline error shows. Registered with the shell, which invokes it.
42
- ctx.registerRevealError(() => {
43
- const firstBlock = Object.keys(blockErrors.value)[0]
44
- if (firstBlock) tree.select(firstBlock)
45
- else if (selectedId.value) tree.select(null)
46
- })
47
-
48
- // Cmd/Ctrl+Z undo, Cmd/Ctrl+Shift+Z (or Ctrl+Y) redo — for the block editor. Skipped while editing text
49
- // (input/textarea/contenteditable) so the field's / richtext's own native undo keeps working.
50
- function onKeydown(e: KeyboardEvent) {
51
- if (!(e.metaKey || e.ctrlKey)) return
52
- const k = e.key.toLowerCase()
53
- const isRedo = k === 'y' || (k === 'z' && e.shiftKey)
54
- const isUndo = k === 'z' && !e.shiftKey
55
- if (!isUndo && !isRedo) return
56
- const el = e.target as HTMLElement | null
57
- if (el && (el.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName))) return
58
- e.preventDefault()
59
- if (isRedo) ctx.redo()
60
- else ctx.undo()
61
- }
62
- onMounted(() => window.addEventListener('keydown', onKeydown))
63
- onUnmounted(() => window.removeEventListener('keydown', onKeydown))
64
- </script>
65
-
66
- <template>
67
- <div class="editor3">
68
- <nav class="editor3__tree" :aria-label="t('blocks.treeLabel')">
69
- <div class="editor3__tree-head">
70
- <p class="editor3__pane-label">{{ t('blocks.treeLabel') }}</p>
71
- </div>
72
- <KestrelBlockTree
73
- root
74
- :blocks="tree.blocks.value"
75
- :selected-id="selectedId"
76
- :error-ids="errorIds"
77
- :dead-ref-ids="deadRefIds"
78
- :ctx="treeCtx"
79
- :disabled="saving"
80
- />
81
- </nav>
82
-
83
- <aside class="editor3__preview" :aria-label="t('preview.ariaLabel')">
84
- <!-- The "Preview" pane label is folded into BlockPreview's own toolbar row (single-line header). -->
85
- <KestrelBlockPreview :content="content" :locale="locale" :selected-id="selectedId" @select="tree.select" />
86
- </aside>
87
-
88
- <section class="editor3__fields" :aria-label="t('blocks.fieldsLabel')">
89
- <!-- A block is selected → its fields; otherwise the page (collection) fields + locale switcher. -->
90
- <KestrelBlockFields
91
- v-if="selectedBlock"
92
- :block="selectedBlock"
93
- :def="byName[selectedBlock.type]"
94
- :locale="locale"
95
- :errors="selectedId ? blockErrors[selectedId] : undefined"
96
- :dead-fields="blockDeadFields"
97
- :disabled="saving"
98
- @update="(k, v) => selectedId && tree.setProp(selectedId, k, v)"
99
- />
100
- <template v-else>
101
- <p class="editor3__pane-label">{{ t('blocks.pageFields') }}</p>
102
- <KestrelPageFieldsPane v-bind="ctx.pageFieldsBindings.value" v-on="ctx.pageFieldsHandlers" />
103
- </template>
104
- </section>
105
- </div>
106
- </template>
107
-
108
- <style lang="scss">
109
- .editor3 {
110
- display: grid;
111
- grid-template-columns: minmax(0, 17rem) minmax(0, 1fr) minmax(0, 22rem);
112
- gap: var(--space-4);
113
- flex: 1 1 auto;
114
- min-height: 0;
115
-
116
- @media (max-width: 1100px) {
117
- grid-template-columns: 1fr;
118
- // Stacked single column: the grid scrolls as a whole; panes grow to their content.
119
- overflow-y: auto;
120
- .editor3__tree,
121
- .editor3__preview,
122
- .editor3__fields {
123
- overflow: visible;
124
- }
125
- }
126
-
127
- &__pane-label {
128
- margin: 0;
129
- font-size: var(--text-sm);
130
- font-weight: var(--weight-medium);
131
- color: var(--color-text-muted);
132
- }
133
-
134
- &__tree-head {
135
- display: flex;
136
- align-items: center;
137
- justify-content: space-between;
138
- gap: var(--space-2);
139
- }
140
-
141
- // Each pane fills the column height and scrolls independently. Tree + fields are the side panels;
142
- // the preview in the middle is the canvas they act on.
143
- &__tree,
144
- &__fields,
145
- &__preview {
146
- min-height: 0;
147
- overflow-y: auto;
148
- display: flex;
149
- flex-direction: column;
150
- gap: var(--space-3);
151
- padding: var(--space-4);
152
- border: 1px solid var(--color-border);
153
- border-radius: var(--radius-lg);
154
- background: var(--color-surface);
155
- }
156
- &__fields {
157
- gap: var(--space-4);
158
- }
159
- &__preview {
160
- background: var(--color-bg);
161
- }
162
- }
163
- </style>
@@ -1,38 +0,0 @@
1
- <script setup lang="ts">
2
- import type { BatchDeleteReport } from '../utils/collection-ops'
3
-
4
- // The destructive-action confirm (replaces window.confirm). Shows how many rows will be deleted and, when
5
- // any are still referenced by other records, the aggregated "left with a broken link" warning fetched from
6
- // the referrer endpoint. Mirrors media/MediaDeleteDialog one layer over.
7
- defineProps<{ open: boolean; report: BatchDeleteReport | null; busy?: boolean; error?: string | null }>()
8
- const emit = defineEmits<{ confirm: []; 'update:open': [boolean] }>()
9
-
10
- const { t } = useT()
11
- </script>
12
-
13
- <template>
14
- <KestrelUiDialog :open="open" :title="t('common.delete')" @update:open="(v) => emit('update:open', v)">
15
- <p v-if="report">{{ t('list.deleteSummary', { n: report.count }) }}</p>
16
- <div v-if="report && report.referencedCount > 0" class="collection-delete__warn">
17
- <p>{{ t('list.deleteReferrersWarn', { n: report.referencedCount }) }}</p>
18
- </div>
19
- <!-- The referrer lookup FAILED (not "none found"): inbound links are unknown, so caution rather than
20
- imply a safe delete. Distinct from the reference warning above, which needs a successful check. -->
21
- <KestrelUiAlert v-if="report && report.checked === false" variant="warning" class="collection-delete__caution">
22
- {{ t('list.deleteRefsUnverified') }}
23
- </KestrelUiAlert>
24
- <KestrelUiAlert v-if="error" variant="error">{{ error }}</KestrelUiAlert>
25
- <template #footer>
26
- <KestrelUiButton variant="ghost" :disabled="busy" @click="emit('update:open', false)">{{ t('common.cancel') }}</KestrelUiButton>
27
- <KestrelUiButton variant="danger" :disabled="busy" @click="emit('confirm')">{{ t('common.delete') }}</KestrelUiButton>
28
- </template>
29
- </KestrelUiDialog>
30
- </template>
31
-
32
- <style lang="scss" scoped>
33
- .collection-delete__warn {
34
- margin-top: var(--space-3);
35
- color: var(--color-danger);
36
- font-size: var(--text-sm);
37
- }
38
- </style>