@michaelthielemann/kestrel 4.1.3 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (340) hide show
  1. package/README.md +26 -201
  2. package/dist/boot.d.ts +45 -0
  3. package/dist/boot.js +274 -0
  4. package/dist/cast.d.ts +3 -0
  5. package/dist/cast.js +6 -0
  6. package/dist/catalogue.d.ts +11 -0
  7. package/dist/catalogue.js +1 -0
  8. package/dist/cli.d.ts +2 -0
  9. package/dist/cli.js +55 -0
  10. package/dist/context.d.ts +62 -0
  11. package/dist/context.js +61 -0
  12. package/dist/dataflow.d.ts +3 -0
  13. package/dist/dataflow.js +65 -0
  14. package/dist/defineConfig.d.ts +157 -0
  15. package/dist/defineConfig.js +41 -0
  16. package/dist/defineContract.d.ts +12 -0
  17. package/dist/defineContract.js +16 -0
  18. package/dist/defineModule.d.ts +75 -0
  19. package/dist/defineModule.js +9 -0
  20. package/dist/definePipeline.d.ts +7 -0
  21. package/dist/definePipeline.js +11 -0
  22. package/dist/errors.d.ts +26 -0
  23. package/dist/errors.js +48 -0
  24. package/dist/index.d.ts +19 -0
  25. package/dist/index.js +14 -0
  26. package/dist/load.d.ts +7 -0
  27. package/dist/load.js +79 -0
  28. package/dist/logger.d.ts +18 -0
  29. package/dist/logger.js +36 -0
  30. package/dist/registry.d.ts +14 -0
  31. package/dist/registry.js +87 -0
  32. package/dist/result.d.ts +19 -0
  33. package/dist/result.js +19 -0
  34. package/dist/runner.d.ts +29 -0
  35. package/dist/runner.js +133 -0
  36. package/dist/schema.d.ts +7 -0
  37. package/dist/schema.js +142 -0
  38. package/dist/sort.d.ts +2 -0
  39. package/dist/sort.js +50 -0
  40. package/dist/testing/runPipeline.d.ts +9 -0
  41. package/dist/testing/runPipeline.js +22 -0
  42. package/dist/triggers/cron.d.ts +10 -0
  43. package/dist/triggers/cron.js +44 -0
  44. package/dist/triggers/cronMatch.d.ts +10 -0
  45. package/dist/triggers/cronMatch.js +68 -0
  46. package/dist/triggers/http.d.ts +58 -0
  47. package/dist/triggers/http.js +292 -0
  48. package/package.json +25 -151
  49. package/NOTICE +0 -12
  50. package/kestrel.config.ts +0 -28
  51. package/layers/access/nuxt.config.ts +0 -5
  52. package/layers/access/server/middleware/00.ip-allowlist.ts +0 -52
  53. package/layers/access/server/middleware/access-guard.ts +0 -32
  54. package/layers/access/server/types.d.ts +0 -7
  55. package/layers/admin/app/components/AdminAccount.vue +0 -212
  56. package/layers/admin/app/components/AdminNav.vue +0 -44
  57. package/layers/admin/app/components/BlockFields.vue +0 -67
  58. package/layers/admin/app/components/BlockPreview.vue +0 -413
  59. package/layers/admin/app/components/BlockTree.vue +0 -433
  60. package/layers/admin/app/components/BlocksBody.vue +0 -163
  61. package/layers/admin/app/components/CollectionDeleteDialog.vue +0 -38
  62. package/layers/admin/app/components/CollectionEditor.vue +0 -324
  63. package/layers/admin/app/components/CollectionList.vue +0 -429
  64. package/layers/admin/app/components/CollectionListBulkBar.vue +0 -62
  65. package/layers/admin/app/components/CollectionListColumnsPanel.vue +0 -37
  66. package/layers/admin/app/components/CollectionListFilterPanel.vue +0 -145
  67. package/layers/admin/app/components/CollectionListPager.vue +0 -70
  68. package/layers/admin/app/components/CollectionListTable.vue +0 -347
  69. package/layers/admin/app/components/EditorStatus.vue +0 -211
  70. package/layers/admin/app/components/EditorUnsupported.vue +0 -28
  71. package/layers/admin/app/components/FieldsBody.vue +0 -23
  72. package/layers/admin/app/components/LocaleBar.vue +0 -107
  73. package/layers/admin/app/components/PageFields.vue +0 -129
  74. package/layers/admin/app/components/PageFieldsPane.vue +0 -39
  75. package/layers/admin/app/components/SeoFields.vue +0 -210
  76. package/layers/admin/app/components/SingletonEditor.vue +0 -93
  77. package/layers/admin/app/composables/useAuth.ts +0 -68
  78. package/layers/admin/app/composables/useBlockTree.ts +0 -95
  79. package/layers/admin/app/composables/useBlocks.ts +0 -7
  80. package/layers/admin/app/composables/useCachedResource.ts +0 -35
  81. package/layers/admin/app/composables/useCollectionOps.ts +0 -102
  82. package/layers/admin/app/composables/useCollections.ts +0 -6
  83. package/layers/admin/app/composables/useContentLocales.ts +0 -23
  84. package/layers/admin/app/composables/useEditForm.ts +0 -402
  85. package/layers/admin/app/composables/useLinkResolver.ts +0 -40
  86. package/layers/admin/app/composables/useListBatchActions.ts +0 -78
  87. package/layers/admin/app/composables/useListColumns.ts +0 -53
  88. package/layers/admin/app/composables/useListFilterDraft.ts +0 -129
  89. package/layers/admin/app/composables/useListRows.ts +0 -75
  90. package/layers/admin/app/composables/useListSelection.ts +0 -28
  91. package/layers/admin/app/composables/useListUrlState.ts +0 -53
  92. package/layers/admin/app/composables/usePendingPublishPoll.ts +0 -15
  93. package/layers/admin/app/composables/usePreviewViewport.ts +0 -30
  94. package/layers/admin/app/composables/usePublishStatus.ts +0 -107
  95. package/layers/admin/app/composables/useRailCollapsed.ts +0 -14
  96. package/layers/admin/app/composables/useTheme.ts +0 -36
  97. package/layers/admin/app/composables/useToolbarPanel.ts +0 -38
  98. package/layers/admin/app/composables/useUnsavedGuard.ts +0 -22
  99. package/layers/admin/app/layouts/admin.vue +0 -266
  100. package/layers/admin/app/middleware/admin-auth.ts +0 -4
  101. package/layers/admin/app/pages/admin/[collection]/[id].vue +0 -203
  102. package/layers/admin/app/pages/admin/[collection]/index.vue +0 -62
  103. package/layers/admin/app/pages/admin/index.vue +0 -118
  104. package/layers/admin/app/pages/admin/login.vue +0 -68
  105. package/layers/admin/app/pages/admin/media.vue +0 -28
  106. package/layers/admin/app/pages/admin/references.vue +0 -146
  107. package/layers/admin/app/plugins/admin-style-guard.client.ts +0 -76
  108. package/layers/admin/app/plugins/reauth.client.ts +0 -20
  109. package/layers/admin/app/utils/admin-style-guard.ts +0 -20
  110. package/layers/admin/app/utils/block-tree.ts +0 -214
  111. package/layers/admin/app/utils/collection-ops.ts +0 -69
  112. package/layers/admin/app/utils/dead-refs.ts +0 -30
  113. package/layers/admin/app/utils/edit-form.ts +0 -267
  114. package/layers/admin/app/utils/editor-expose.ts +0 -36
  115. package/layers/admin/app/utils/editor-form-context.ts +0 -82
  116. package/layers/admin/app/utils/editor-registry.ts +0 -20
  117. package/layers/admin/app/utils/field-keys.ts +0 -11
  118. package/layers/admin/app/utils/list-cell.ts +0 -46
  119. package/layers/admin/app/utils/list-columns.ts +0 -142
  120. package/layers/admin/app/utils/list-query.ts +0 -96
  121. package/layers/admin/app/utils/nav-active.ts +0 -9
  122. package/layers/admin/app/utils/populate-blocks.ts +0 -288
  123. package/layers/admin/app/utils/preview-channel.ts +0 -62
  124. package/layers/admin/app/utils/preview-viewport.ts +0 -80
  125. package/layers/admin/app/utils/reauth.ts +0 -67
  126. package/layers/admin/app/utils/record-title.ts +0 -15
  127. package/layers/admin/app/utils/register-builtin-editors.ts +0 -9
  128. package/layers/admin/app/utils/safe-redirect.ts +0 -12
  129. package/layers/admin/nuxt.config.ts +0 -20
  130. package/layers/auth/nuxt.config.ts +0 -1
  131. package/layers/auth/server/plugins/01.register-auth-pipelines.ts +0 -7
  132. package/layers/collections/nuxt.config.ts +0 -1
  133. package/layers/collections/server/plugins/01.register.ts +0 -29
  134. package/layers/collections/server/plugins/02.register-relation-populate.ts +0 -18
  135. package/layers/core/app/composables/layouts.ts +0 -5
  136. package/layers/core/app/utils/field-factories.ts +0 -129
  137. package/layers/core/app/utils/layouts.ts +0 -38
  138. package/layers/core/modules/auto-discovery/extract-block.ts +0 -179
  139. package/layers/core/modules/auto-discovery/index.ts +0 -112
  140. package/layers/core/modules/auto-discovery/package-registry.ts +0 -19
  141. package/layers/core/modules/auto-discovery/scan.ts +0 -170
  142. package/layers/core/modules/auto-discovery/virtual.d.ts +0 -20
  143. package/layers/core/modules/component-namespace/index.ts +0 -44
  144. package/layers/core/modules/component-namespace/shared.ts +0 -24
  145. package/layers/core/modules/kestrel/app-shell.ts +0 -55
  146. package/layers/core/modules/kestrel/auto-imports.ts +0 -44
  147. package/layers/core/modules/kestrel/index.ts +0 -144
  148. package/layers/core/modules/plugin-order/index.ts +0 -69
  149. package/layers/core/modules/plugin-order/plugin-order.ts +0 -273
  150. package/layers/core/nuxt.config.ts +0 -10
  151. package/layers/core/server/api/[...path].ts +0 -98
  152. package/layers/core/server/plugins/00.config.ts +0 -12
  153. package/layers/core/server/plugins/00.migrate.ts +0 -12
  154. package/layers/core/server/plugins/01.register-introspection-pipeline.ts +0 -5
  155. package/layers/core/server/plugins/01.register-openapi-pipeline.ts +0 -5
  156. package/layers/core/server/plugins/01.register-tooling-pipelines.ts +0 -5
  157. package/layers/core/server/plugins/02.schema-sync.ts +0 -58
  158. package/layers/core/server/plugins/04.outbox-worker.ts +0 -31
  159. package/layers/core/server/plugins/05.reindex-refs.ts +0 -5
  160. package/layers/core/server/routes/__kestrel/dashboard.get.ts +0 -29
  161. package/layers/core/server/tasks/db/migrate-module.ts +0 -83
  162. package/layers/core/server/tasks/db/migrate-revisions.ts +0 -20
  163. package/layers/core/server/tasks/db/migrate.ts +0 -50
  164. package/layers/core/server/utils/server-config.ts +0 -81
  165. package/layers/fields/nuxt.config.ts +0 -1
  166. package/layers/fields/server/plugins/01.register-blocks-pipeline.ts +0 -7
  167. package/layers/fields/server/plugins/01.register-field-populate.ts +0 -11
  168. package/layers/media/app/components/KestrelImg.vue +0 -94
  169. package/layers/media/app/components/MediaConflictDialog.vue +0 -35
  170. package/layers/media/app/components/MediaDeleteDialog.vue +0 -52
  171. package/layers/media/app/components/MediaGrid.vue +0 -113
  172. package/layers/media/app/components/MediaImage.vue +0 -70
  173. package/layers/media/app/components/MediaLibrary.vue +0 -456
  174. package/layers/media/app/components/MediaNewFolderDialog.vue +0 -32
  175. package/layers/media/app/components/MediaPathBar.vue +0 -163
  176. package/layers/media/app/components/MediaPicker.vue +0 -19
  177. package/layers/media/app/components/MediaRenameDialog.vue +0 -32
  178. package/layers/media/app/components/MediaTable.vue +0 -75
  179. package/layers/media/app/components/MediaThumb.vue +0 -25
  180. package/layers/media/app/components/MediaToolbar.vue +0 -90
  181. package/layers/media/app/components/MediaUploadDialog.vue +0 -87
  182. package/layers/media/app/components/MediaViewer.vue +0 -137
  183. package/layers/media/app/components/field/Media.vue +0 -263
  184. package/layers/media/app/composables/useBlurUp.ts +0 -55
  185. package/layers/media/app/composables/useMediaClipboard.ts +0 -14
  186. package/layers/media/app/composables/useMediaContextMenu.ts +0 -56
  187. package/layers/media/app/composables/useMediaDnd.ts +0 -56
  188. package/layers/media/app/composables/useMediaLibrary.ts +0 -152
  189. package/layers/media/app/composables/useMediaOps.ts +0 -50
  190. package/layers/media/app/composables/useMediaRelocate.ts +0 -39
  191. package/layers/media/app/composables/useMediaResolver.ts +0 -50
  192. package/layers/media/app/composables/useMediaUpload.ts +0 -93
  193. package/layers/media/app/composables/useMediaVariant.ts +0 -34
  194. package/layers/media/app/plugins/field-media.client.ts +0 -6
  195. package/layers/media/app/utils/ai-disclosure.ts +0 -19
  196. package/layers/media/app/utils/dnd.ts +0 -69
  197. package/layers/media/app/utils/library.ts +0 -81
  198. package/layers/media/app/utils/ops.ts +0 -60
  199. package/layers/media/app/utils/picture-model.ts +0 -125
  200. package/layers/media/app/utils/thumbhash.ts +0 -19
  201. package/layers/media/app/utils/variant-name.ts +0 -20
  202. package/layers/media/nuxt.config.ts +0 -7
  203. package/layers/media/server/middleware/ondemand-variants.ts +0 -38
  204. package/layers/media/server/plugins/01.register-media-pipelines.ts +0 -7
  205. package/layers/media/server/plugins/02.register-media.ts +0 -22
  206. package/layers/media/server/plugins/04.variant-capture.ts +0 -27
  207. package/layers/media/server/plugins/05.media-cleanup.ts +0 -5
  208. package/layers/media/server/tasks/media/backfill.ts +0 -17
  209. package/layers/public/app/app.vue +0 -5
  210. package/layers/public/app/components/BlockRenderer.vue +0 -56
  211. package/layers/public/app/components/KestrelLink.vue +0 -26
  212. package/layers/public/app/components/KestrelPreviewBridge.vue +0 -133
  213. package/layers/public/app/composables/public-page.ts +0 -24
  214. package/layers/public/app/error.vue +0 -12
  215. package/layers/public/app/layouts/default.vue +0 -5
  216. package/layers/public/app/pages/[...slug].vue +0 -239
  217. package/layers/public/app/pages/__kestrel/preview.vue +0 -46
  218. package/layers/public/app/utils/block-edit-context.ts +0 -16
  219. package/layers/public/app/utils/json-ld.ts +0 -139
  220. package/layers/public/app/utils/link-href.ts +0 -46
  221. package/layers/public/app/utils/page-head.ts +0 -84
  222. package/layers/public/app/utils/page-layout.ts +0 -18
  223. package/layers/public/app/utils/preview-protocol.ts +0 -117
  224. package/layers/public/app/utils/public-route.ts +0 -46
  225. package/layers/public/app/utils/site-head.ts +0 -43
  226. package/layers/public/modules/deploy-output/index.ts +0 -80
  227. package/layers/public/modules/prerender-routes/discover.ts +0 -37
  228. package/layers/public/modules/prerender-routes/index.ts +0 -88
  229. package/layers/public/modules/prerender-routes/routes.ts +0 -16
  230. package/layers/public/modules/prune-media/index.ts +0 -48
  231. package/layers/public/modules/prune-media/prune-media.ts +0 -81
  232. package/layers/public/nuxt.config.ts +0 -14
  233. package/layers/public/server/delivery-live/serve.ts +0 -111
  234. package/layers/public/server/middleware/delivery-live-catchall.ts +0 -7
  235. package/layers/public/server/pipelines/index.ts +0 -23
  236. package/layers/public/server/plugins/00.ensure-snapshot-triggers.ts +0 -14
  237. package/layers/public/server/plugins/01.register-public-pipelines.ts +0 -7
  238. package/layers/public/server/plugins/02.register-links.ts +0 -16
  239. package/layers/public/server/plugins/03.redirects.ts +0 -55
  240. package/layers/public/server/plugins/05.plan-publish.ts +0 -5
  241. package/layers/public/server/plugins/zz.publish.ts +0 -127
  242. package/layers/public/server/routes/llms-full.txt.get.ts +0 -99
  243. package/layers/public/server/routes/llms.txt.get.ts +0 -66
  244. package/layers/public/server/routes/redirects.json.get.ts +0 -58
  245. package/layers/public/server/routes/robots.txt.get.ts +0 -11
  246. package/layers/public/server/routes/sitemap.xml.get.ts +0 -79
  247. package/layers/public/server/tasks/publish/run.ts +0 -32
  248. package/layers/public/server/utils/publish/render-live.ts +0 -26
  249. package/layers/ui/app/assets/scss/_base.scss +0 -16
  250. package/layers/ui/app/assets/scss/_datepicker.scss +0 -82
  251. package/layers/ui/app/assets/scss/_mixins.scss +0 -50
  252. package/layers/ui/app/assets/scss/_reset.scss +0 -65
  253. package/layers/ui/app/assets/scss/_tokens.scss +0 -183
  254. package/layers/ui/app/assets/scss/main.scss +0 -4
  255. package/layers/ui/app/components/field/Boolean.vue +0 -30
  256. package/layers/ui/app/components/field/Choice.vue +0 -73
  257. package/layers/ui/app/components/field/Datetime.vue +0 -98
  258. package/layers/ui/app/components/field/DeadRefNote.vue +0 -28
  259. package/layers/ui/app/components/field/Json.vue +0 -62
  260. package/layers/ui/app/components/field/Layout.vue +0 -138
  261. package/layers/ui/app/components/field/Link.vue +0 -112
  262. package/layers/ui/app/components/field/LinkInternalPicker.vue +0 -95
  263. package/layers/ui/app/components/field/Number.vue +0 -30
  264. package/layers/ui/app/components/field/Relation.vue +0 -49
  265. package/layers/ui/app/components/field/Renderer.vue +0 -30
  266. package/layers/ui/app/components/field/Repeater.vue +0 -460
  267. package/layers/ui/app/components/field/Richtext.vue +0 -26
  268. package/layers/ui/app/components/field/Slug.vue +0 -65
  269. package/layers/ui/app/components/field/Text.vue +0 -35
  270. package/layers/ui/app/components/field/Unsupported.vue +0 -34
  271. package/layers/ui/app/components/ui/Alert.vue +0 -50
  272. package/layers/ui/app/components/ui/Brand.vue +0 -22
  273. package/layers/ui/app/components/ui/Button.vue +0 -116
  274. package/layers/ui/app/components/ui/ButtonGroup.vue +0 -96
  275. package/layers/ui/app/components/ui/Checkbox.vue +0 -53
  276. package/layers/ui/app/components/ui/CheckboxGroup.vue +0 -51
  277. package/layers/ui/app/components/ui/Combobox.vue +0 -345
  278. package/layers/ui/app/components/ui/DatePicker.vue +0 -92
  279. package/layers/ui/app/components/ui/DateRangePicker.vue +0 -100
  280. package/layers/ui/app/components/ui/Dialog.vue +0 -107
  281. package/layers/ui/app/components/ui/EmptyState.vue +0 -46
  282. package/layers/ui/app/components/ui/Field.vue +0 -58
  283. package/layers/ui/app/components/ui/Fieldset.vue +0 -65
  284. package/layers/ui/app/components/ui/Icon.vue +0 -44
  285. package/layers/ui/app/components/ui/Menu.vue +0 -54
  286. package/layers/ui/app/components/ui/NumberInput.vue +0 -175
  287. package/layers/ui/app/components/ui/Richtext.vue +0 -141
  288. package/layers/ui/app/components/ui/RichtextToolbar.vue +0 -251
  289. package/layers/ui/app/components/ui/Select.vue +0 -52
  290. package/layers/ui/app/components/ui/TextInput.vue +0 -111
  291. package/layers/ui/app/components/ui/Textarea.vue +0 -39
  292. package/layers/ui/app/components/ui/TimeInput.vue +0 -18
  293. package/layers/ui/app/components/ui/Toasts.vue +0 -103
  294. package/layers/ui/app/components/ui/Tooltip.vue +0 -45
  295. package/layers/ui/app/composables/useAdminLang.ts +0 -8
  296. package/layers/ui/app/composables/useDragReorder.ts +0 -41
  297. package/layers/ui/app/composables/useEchoGuard.ts +0 -28
  298. package/layers/ui/app/composables/useLinkField.ts +0 -70
  299. package/layers/ui/app/composables/useRecordOptions.ts +0 -84
  300. package/layers/ui/app/composables/useRepeater.ts +0 -99
  301. package/layers/ui/app/composables/useT.ts +0 -32
  302. package/layers/ui/app/composables/useToast.ts +0 -46
  303. package/layers/ui/app/i18n/de.ts +0 -432
  304. package/layers/ui/app/i18n/en.ts +0 -434
  305. package/layers/ui/app/utils/boolean-field.ts +0 -10
  306. package/layers/ui/app/utils/field-component.ts +0 -22
  307. package/layers/ui/app/utils/field-empty.ts +0 -21
  308. package/layers/ui/app/utils/field-registry.ts +0 -36
  309. package/layers/ui/app/utils/humanize.ts +0 -14
  310. package/layers/ui/app/utils/icons.ts +0 -143
  311. package/layers/ui/app/utils/iso-date.ts +0 -18
  312. package/layers/ui/app/utils/localized.ts +0 -13
  313. package/layers/ui/app/utils/record-context.ts +0 -15
  314. package/layers/ui/app/utils/reorder.ts +0 -8
  315. package/layers/ui/app/utils/richtext-preserve-class.ts +0 -31
  316. package/layers/ui/app/utils/useFieldA11y.ts +0 -40
  317. package/layers/ui/nuxt.config.ts +0 -9
  318. package/nuxt.config.ts +0 -108
  319. package/scripts/consumer-template-ci.mjs +0 -160
  320. package/scripts/copy-create-payload.mjs +0 -49
  321. package/scripts/coverage-all.mjs +0 -163
  322. package/scripts/dashboard.mjs +0 -89
  323. package/scripts/hash-password.mjs +0 -29
  324. package/scripts/kestrel.mjs +0 -240
  325. package/scripts/lib/cli.mjs +0 -129
  326. package/scripts/lib/coverage-model.mjs +0 -51
  327. package/scripts/lib/password.mjs +0 -19
  328. package/scripts/lib/scaffold.mjs +0 -205
  329. package/scripts/port-graph.mjs +0 -350
  330. package/scripts/typecheck.mjs +0 -62
  331. package/templates/starter/README.md +0 -65
  332. package/templates/starter/_env.example +0 -17
  333. package/templates/starter/_gitignore +0 -26
  334. package/templates/starter/_package.json +0 -22
  335. package/templates/starter/app/app.vue +0 -7
  336. package/templates/starter/app/blocks/Prose.vue +0 -13
  337. package/templates/starter/app/layouts/default.vue +0 -6
  338. package/templates/starter/nuxt.config.ts +0 -23
  339. package/templates/starter/pnpm-workspace.yaml +0 -8
  340. package/templates/starter/tsconfig.json +0 -3
@@ -1,70 +0,0 @@
1
- <script setup lang="ts">
2
- import { PER_PAGE_OPTIONS } from '@michaelthielemann/kestrel-core/client'
3
-
4
- const props = defineProps<{ page: number; totalPages: number; total: number; perPage: number }>()
5
- const emit = defineEmits<{ 'update:page': [page: number]; 'update:perPage': [perPage: number] }>()
6
-
7
- const { t } = useT()
8
- // The per-page `<select>` is hand-rolled here (no UiField wrapper), so the label↔control association needs
9
- // an explicit id pair for static a11y analysis to see it.
10
- const perPageId = useId()
11
-
12
- function prev() {
13
- if (props.page > 1) emit('update:page', props.page - 1)
14
- }
15
- function next() {
16
- if (props.page < props.totalPages) emit('update:page', props.page + 1)
17
- }
18
- </script>
19
-
20
- <template>
21
- <div class="list__pager">
22
- <KestrelUiButton type="button" size="sm" :disabled="page <= 1" @click="prev">{{ t('list.prev') }}</KestrelUiButton>
23
- <span class="list__page">{{ t('list.page', { page, totalPages, total }) }}</span>
24
- <KestrelUiButton type="button" size="sm" :disabled="page >= totalPages" @click="next">{{ t('list.next') }}</KestrelUiButton>
25
- <label class="list__perpage" :for="perPageId">
26
- <span class="list__perpage-label">{{ t('list.perPage') }}</span>
27
- <select
28
- :id="perPageId"
29
- class="list__perpage-select"
30
- :value="perPage"
31
- :aria-label="t('list.perPage')"
32
- @change="emit('update:perPage', Number(($event.target as HTMLSelectElement).value))"
33
- >
34
- <option v-for="n in PER_PAGE_OPTIONS" :key="n" :value="n">{{ n }}</option>
35
- </select>
36
- </label>
37
- </div>
38
- </template>
39
-
40
- <style lang="scss">
41
- .list {
42
- &__pager {
43
- display: flex;
44
- align-items: center;
45
- gap: var(--space-3);
46
- }
47
- &__page {
48
- font-size: var(--text-sm);
49
- color: var(--color-text-muted);
50
- }
51
- // Per-page size selector sits at the far end of the pager.
52
- &__perpage {
53
- display: inline-flex;
54
- align-items: center;
55
- gap: var(--space-2);
56
- margin-inline-start: auto;
57
- font-size: var(--text-sm);
58
- color: var(--color-text-muted);
59
- }
60
- &__perpage-select {
61
- padding: var(--space-1) var(--space-2);
62
- border: 1px solid var(--color-border);
63
- border-radius: var(--radius-sm);
64
- background: var(--color-surface);
65
- color: var(--color-text);
66
- font: inherit;
67
- font-size: var(--text-sm);
68
- }
69
- }
70
- </style>
@@ -1,347 +0,0 @@
1
- <script setup lang="ts">
2
- import { sortDirection } from '../utils/list-query'
3
- import { cellDisplay, columnLabel, rowLabel as labelForRow } from '../utils/list-cell'
4
- import type { ListColumn } from '../utils/list-columns'
5
- import { resolveLocalized } from '../../../ui/app/utils/localized'
6
- import type { SerializedAction } from '@michaelthielemann/kestrel-core'
7
-
8
- const props = defineProps<{
9
- rows: Record<string, unknown>[]
10
- columns: ListColumn[]
11
- collection: string
12
- localeQuery: string
13
- sort: string
14
- busy: boolean
15
- selected: Set<number>
16
- allSelected: boolean
17
- headerIndeterminate: boolean
18
- /** Schema-driven row actions beyond the built-in edit/duplicate/delete. */
19
- actions?: SerializedAction[]
20
- }>()
21
- const emit = defineEmits<{
22
- sort: [key: string]
23
- toggleRow: [id: number, on: boolean]
24
- toggleAll: [on: boolean]
25
- duplicate: [id: number]
26
- delete: [ids: number[]]
27
- runAction: [action: SerializedAction, id: number]
28
- }>()
29
-
30
- const { t, lang } = useT()
31
- const { locales } = useContentLocales()
32
-
33
- const colLabel = (col: ListColumn) => columnLabel(col, t)
34
- const rowLabel = (row: Record<string, unknown>) => labelForRow(props.columns, row)
35
- const actionLabel = (action: SerializedAction) => resolveLocalized(action.label, lang.value) ?? action.name
36
-
37
- function arrow(field: string) {
38
- const dir = sortDirection(props.sort, field)
39
- return dir === 'asc' ? ' ▲' : dir === 'desc' ? ' ▼' : ''
40
- }
41
- // Programmatic sort state for AT (the visual arrow is aria-hidden) — WCAG 1.3.1 / 4.1.2.
42
- function ariaSort(field: string): 'ascending' | 'descending' | 'none' {
43
- const dir = sortDirection(props.sort, field)
44
- return dir === 'asc' ? 'ascending' : dir === 'desc' ? 'descending' : 'none'
45
- }
46
-
47
- // Per-row translation status (the list query attaches `$translations`: locale → sibling id, or null).
48
- // Present locales link to their sibling; missing ones offer create-and-link, carrying the translation group.
49
- function rowTranslations(row: Record<string, unknown>) {
50
- const map = (row.$translations ?? {}) as Record<string, number | null>
51
- const group = row.translationGroup as string | undefined
52
- return locales.map((loc) => {
53
- const id = map[loc]
54
- return id != null
55
- ? { loc, present: true, to: `/admin/${props.collection}/${id}?locale=${loc}` }
56
- : { loc, present: false, to: `/admin/${props.collection}/new?locale=${loc}${group ? `&group=${group}` : ''}` }
57
- })
58
- }
59
- </script>
60
-
61
- <template>
62
- <table class="list__table">
63
- <thead>
64
- <tr>
65
- <th class="list__select-th" scope="col">
66
- <KestrelUiCheckbox
67
- :model-value="allSelected"
68
- :indeterminate="headerIndeterminate"
69
- :aria-label="t('list.selectAll')"
70
- @update:model-value="(v) => emit('toggleAll', v)"
71
- />
72
- </th>
73
- <!-- Row actions: an always-visible, left-frozen chrome column placed right after Select. Its
74
- accessible name is real visually-hidden text, not aria-label on a static <th> (which several
75
- screen-reader / browser pairs ignore on non-interactive table cells). -->
76
- <th class="list__actions-th" scope="col">
77
- <span class="list__vh">{{ t('a11y.rowActions') }}</span>
78
- </th>
79
- <th
80
- v-for="c in columns"
81
- :key="c.key"
82
- scope="col"
83
- :aria-sort="c.sortable ? ariaSort(c.key) : undefined"
84
- :class="{ 'list__th-narrow': c.type === 'translations' || c.type === 'deadRefs' }"
85
- >
86
- <button v-if="c.sortable" type="button" class="list__sort" @click="emit('sort', c.key)">{{ colLabel(c) }}<span aria-hidden="true">{{ arrow(c.key) }}</span></button>
87
- <span v-else>{{ colLabel(c) }}</span>
88
- </th>
89
- </tr>
90
- </thead>
91
- <tbody>
92
- <tr v-for="row in rows" :key="String(row.id)" class="list__row">
93
- <td class="list__select-cell">
94
- <KestrelUiCheckbox
95
- :model-value="selected.has(Number(row.id))"
96
- :aria-label="t('list.selectRow', { name: rowLabel(row) })"
97
- @update:model-value="(v) => emit('toggleRow', Number(row.id), v)"
98
- />
99
- </td>
100
- <td class="list__actions-cell">
101
- <div class="list__row-actions">
102
- <NuxtLink :to="`/admin/${collection}/${row.id}${localeQuery}`" class="list__action-btn" :aria-label="t('list.rowEdit', { name: rowLabel(row) })">
103
- <KestrelUiIcon name="pencil" :size="15" />
104
- </NuxtLink>
105
- <button type="button" class="list__action-btn" :disabled="busy" :aria-label="t('list.rowDuplicate', { name: rowLabel(row) })" @click="emit('duplicate', Number(row.id))">
106
- <KestrelUiIcon name="copy" :size="15" />
107
- </button>
108
- <button
109
- v-for="action in actions"
110
- :key="action.name"
111
- type="button"
112
- class="list__action-btn"
113
- :disabled="busy"
114
- :data-action="action.name"
115
- :aria-label="`${actionLabel(action)}: ${rowLabel(row)}`"
116
- @click="emit('runAction', action, Number(row.id))"
117
- >
118
- <KestrelUiIcon :name="action.icon ?? 'zap'" :size="15" />
119
- </button>
120
- <button type="button" class="list__action-btn list__action-btn--danger" :disabled="busy" :aria-label="t('list.rowDelete', { name: rowLabel(row) })" @click="emit('delete', [Number(row.id)])">
121
- <KestrelUiIcon name="trash" :size="15" />
122
- </button>
123
- </div>
124
- </td>
125
- <!-- A quarantined row carries none of its normal fields — a single spanning cell replaces
126
- the per-column cells rather than rendering blanks for data that doesn't exist. -->
127
- <td v-if="row.$quarantined === true" :colspan="columns.length">
128
- <span class="list__quarantine-badge">
129
- <KestrelUiIcon name="triangle-alert" :size="14" role="img" :aria-label="t('list.quarantined')" />
130
- {{ t('list.quarantined') }}
131
- </span>
132
- </td>
133
- <td v-else v-for="c in columns" :key="c.key" :class="{ 'list__narrow-cell': c.type === 'translations' || c.type === 'deadRefs' }">
134
- <span v-if="c.type === 'deadRefs'" class="list__deadrefs">
135
- <KestrelUiIcon
136
- v-if="row.$hasDeadRefs"
137
- name="triangle-alert"
138
- :size="15"
139
- class="list__deadref-icon"
140
- role="img"
141
- :aria-label="t('deadRefs.listWarning')"
142
- :title="t('deadRefs.listWarning')"
143
- />
144
- </span>
145
- <span v-else-if="c.type === 'translations'" class="list__transl">
146
- <NuxtLink
147
- v-for="tr in rowTranslations(row)"
148
- :key="tr.loc"
149
- :to="tr.to"
150
- :data-loc="tr.loc"
151
- class="list__badge"
152
- :class="tr.present ? 'list__badge--present' : 'list__badge--missing'"
153
- :title="tr.present ? t('list.editLocale', { loc: tr.loc.toUpperCase() }) : t('list.createLocale', { loc: tr.loc.toUpperCase() })"
154
- :aria-label="tr.present ? t('list.editLocale', { loc: tr.loc.toUpperCase() }) : t('list.createLocale', { loc: tr.loc.toUpperCase() })"
155
- >{{ tr.loc.toUpperCase() }}</NuxtLink>
156
- </span>
157
- <template v-else>{{ cellDisplay(c, row) }}</template>
158
- </td>
159
- </tr>
160
- </tbody>
161
- </table>
162
- </template>
163
-
164
- <style lang="scss">
165
- // Visually-hidden text for a static cell that must be announced but not shown (the Actions column header).
166
- // Chosen over aria-label, which is unreliably announced on non-interactive table cells.
167
- .list__vh {
168
- position: absolute;
169
- width: 1px;
170
- height: 1px;
171
- padding: 0;
172
- margin: -1px;
173
- overflow: hidden;
174
- clip: rect(0, 0, 0, 0);
175
- white-space: nowrap;
176
- border: 0;
177
- }
178
-
179
- .list {
180
- &__table {
181
- width: 100%;
182
- border-collapse: collapse;
183
- font-size: var(--text-sm);
184
-
185
- th,
186
- td {
187
- text-align: start;
188
- padding: var(--space-2) var(--space-3);
189
- border-bottom: 1px solid var(--color-border);
190
- vertical-align: top;
191
- }
192
- // Keep the column headers visible while the rows scroll inside .list__scroll. A solid background is
193
- // required so scrolling rows don't show through. Part of the frozen-column z-index stack (see the
194
- // &__select-* / &__actions-* rules): scrolling body td 0 < frozen body td 1 < scrolling header th 2 <
195
- // frozen corner th 3, so nothing bleeds through on either scroll axis.
196
- thead th {
197
- position: sticky;
198
- top: 0;
199
- z-index: 2; // scrolling header: above scrolling body cells (0) and the frozen body columns (1)
200
- background: var(--color-bg);
201
- }
202
- // Frozen corner: the select + actions headers are sticky on BOTH axes, so they must sit above every
203
- // scrolling header AND every frozen body column where the two frozen edges cross.
204
- thead th.list__select-th,
205
- thead th.list__actions-th {
206
- z-index: 3;
207
- }
208
- }
209
- &__sort {
210
- background: none;
211
- border: 0;
212
- padding: 0;
213
- font: inherit;
214
- font-weight: var(--weight-medium);
215
- cursor: pointer;
216
- color: var(--color-text);
217
- }
218
- // Compact, de-emphasised info columns (translations · dead-reference warning).
219
- &__th-narrow,
220
- &__narrow-cell {
221
- width: 1%;
222
- white-space: nowrap;
223
- }
224
-
225
- // Quarantine badge — icon + text (not color alone, WCAG 1.4.1), replacing a row's normal data cells.
226
- &__quarantine-badge {
227
- display: inline-flex;
228
- align-items: center;
229
- gap: var(--space-1);
230
- padding: 2px var(--space-2);
231
- border: 1px solid var(--color-danger);
232
- border-radius: var(--radius-sm);
233
- color: var(--color-danger);
234
- font-size: var(--text-xs);
235
- font-weight: var(--weight-medium);
236
- }
237
-
238
- // Dead-reference warning: an amber triangle, shown only on rows holding a stale reference.
239
- &__deadrefs {
240
- display: inline-flex;
241
- }
242
- &__deadref-icon {
243
- color: var(--color-warning);
244
- }
245
- &__transl {
246
- display: inline-flex;
247
- gap: 2px;
248
- flex-wrap: wrap;
249
- }
250
- &__badge {
251
- display: inline-flex;
252
- align-items: center;
253
- justify-content: center;
254
- min-width: 1.4rem;
255
- padding: 0 3px;
256
- border: 1px solid transparent;
257
- border-radius: var(--radius-sm);
258
- font-size: 0.625rem;
259
- font-weight: var(--weight-medium);
260
- line-height: 1.5;
261
- letter-spacing: 0.02em;
262
- text-decoration: none;
263
-
264
- &--present {
265
- background: var(--color-primary-soft);
266
- color: var(--color-primary);
267
- }
268
- &--missing {
269
- color: var(--color-text-muted);
270
- border-color: var(--color-border);
271
- border-style: dashed;
272
- }
273
- &:focus-visible {
274
- outline: 2px solid var(--color-focus);
275
- outline-offset: -2px;
276
- }
277
- }
278
-
279
- // Selection column — the first frozen chrome column, pinned to the left with `position: sticky` inside
280
- // the existing .list__scroll (no new scroll container). Opaque `var(--color-bg)` so horizontally-scrolled
281
- // data can't bleed through; z-index 1 lifts the frozen body cell above the scrolling body cells.
282
- &__select-th,
283
- &__select-cell {
284
- position: sticky;
285
- left: 0;
286
- z-index: 1;
287
- width: var(--list-col-select);
288
- white-space: nowrap;
289
- vertical-align: middle;
290
- background: var(--color-bg);
291
- }
292
- // Row-actions column — the second frozen column, sitting immediately after Select (its `left` offset is
293
- // exactly the select column's width). Always visible; snug `width: 1%` so the three icon buttons never
294
- // steal width from the Title column.
295
- &__actions-th,
296
- &__actions-cell {
297
- position: sticky;
298
- left: var(--list-col-select);
299
- z-index: 1;
300
- width: 1%;
301
- white-space: nowrap;
302
- vertical-align: middle;
303
- background: var(--color-bg);
304
- }
305
- // A plain, always-visible inline row of icon buttons (Edit → Duplicate → Delete; destructive last, never
306
- // default focus). BlockTree.vue deliberately keeps its own absolute overlay: a tree row is a single flex
307
- // row with no column grid — there is no cell to place actions in.
308
- &__row-actions {
309
- display: inline-flex;
310
- align-items: center;
311
- gap: 1px;
312
- }
313
- &__action-btn {
314
- display: flex;
315
- align-items: center;
316
- justify-content: center;
317
- width: 1.5rem;
318
- height: 1.5rem;
319
- border: 0;
320
- border-radius: var(--radius-sm);
321
- background: transparent;
322
- color: var(--color-text-muted); /* meaningful icon control at rest → ≥3:1 (SC 1.4.11) */
323
- text-decoration: none;
324
- cursor: pointer;
325
- transition:
326
- background-color var(--motion-fast) var(--ease-standard),
327
- color var(--motion-fast) var(--ease-standard);
328
-
329
- &:hover {
330
- background: var(--color-hover);
331
- color: var(--color-text);
332
- }
333
- &:focus-visible {
334
- outline: 2px solid var(--color-focus);
335
- outline-offset: -2px;
336
- }
337
- &--danger:hover {
338
- color: var(--color-danger);
339
- }
340
- &:disabled {
341
- opacity: 0.5;
342
- cursor: default;
343
- pointer-events: none;
344
- }
345
- }
346
- }
347
- </style>
@@ -1,211 +0,0 @@
1
- <script setup lang="ts">
2
- // The editor "Ampel": TWO lamps side by side in the action bar, each a dot + ONE word + a details tooltip.
3
- // LEFT — the SAVE / draft lifecycle of THIS record (its local vs. stored state):
4
- // amber — work in flight: saving, or unsaved local changes not yet in the DB
5
- // blue — saved but a Draft, so it is intentionally NOT on the live/static site
6
- // green — saved and (for status collections) published, or just saved (no status field)
7
- // RIGHT — the GENERATED state of this record's static page (pageLike collections only), read from
8
- // `publish_status` via `/api/publishStatus`:
9
- // green — the route's last publish succeeded (the file is live; the tooltip names local vs S3)
10
- // amber — published but no success row yet — a republish is in flight ("Generating…")
11
- // neutral — this environment never produces the file (dev / static output off) → "Not built"
12
- // red — the last publish for the route errored (render / write / S3) — details in the tooltip
13
- // blue — a Draft: intentionally not generated, so there is no live file
14
- import type { PublishStatusData } from '../composables/usePublishStatus'
15
-
16
- const props = defineProps<{
17
- dirty: boolean
18
- saving?: boolean
19
- hasStatus?: boolean
20
- /** The LIVE (possibly unsaved) form status — drives the left lamp's word. */
21
- status?: string
22
- /** The last SAVED publish status — drives the RIGHT lamp's draft/published decision, so an unsaved
23
- * dropdown switch never flips the generated-state indicator. Falls back to `status` when absent. */
24
- savedStatus?: string
25
- /** Only pageLike collections produce a static page → only they get the right (live) lamp. */
26
- pageLike?: boolean
27
- /** The record's last publish outcome (from `/api/publishStatus`); null/absent while unknown. */
28
- live?: PublishStatusData | null
29
- }>()
30
- const { t, lang } = useT()
31
-
32
- // LEFT lamp — save + publish state. `word` is the single label shown; `detail` is the tooltip sentence.
33
- const save = computed<{ tone: 'amber' | 'green' | 'blue'; word: string; detail: string }>(() => {
34
- if (props.saving) return { tone: 'amber', word: t('editorStatus.word.saving'), detail: t('editorStatus.detail.saving') }
35
- if (props.dirty) return { tone: 'amber', word: t('editorStatus.word.unsaved'), detail: t('editorStatus.detail.unsaved') }
36
- if (props.hasStatus && props.status === 'draft') return { tone: 'blue', word: t('editorStatus.word.draft'), detail: t('editorStatus.detail.draft') }
37
- if (props.hasStatus) return { tone: 'green', word: t('editorStatus.word.published'), detail: t('editorStatus.detail.published') }
38
- return { tone: 'green', word: t('editorStatus.word.saved'), detail: t('editorStatus.detail.saved') }
39
- })
40
-
41
- const updatedAtLabel = computed(() => {
42
- const at = props.live?.updatedAt
43
- if (!at) return ''
44
- const d = new Date(at)
45
- if (Number.isNaN(d.getTime())) return ''
46
- return t('editorStatus.live.updatedAt', { time: d.toLocaleString(lang.value) })
47
- })
48
-
49
- // RIGHT lamp — the live / generated state of this record's static page. Only a SAVED pageLike record with a
50
- // resolved route has one (a non-pageLike collection produces no static page; an unsaved `new` record leaves
51
- // `live.route` null → no right lamp until the record actually has a page).
52
- const live = computed<{ tone: 'green' | 'red' | 'amber' | 'blue' | 'neutral'; word: string; detail: string; when?: string; error?: string } | null>(() => {
53
- if (props.pageLike !== true || !props.live?.route) return null
54
- // Key the draft/published decision off the SAVED status, not the live form value: an unsaved dropdown
55
- // switch must not move the generated-state indicator. Switching a live page → Draft keeps it "Live" (the
56
- // file stays on the server until the save prunes it); switching a saved Draft → Published keeps it "Not
57
- // live" (nothing is generated until the publish is actually saved). A DISTINCT word from the left lamp's
58
- // "Draft" so the two lamps read as two signals (save-state vs generated-state), not a repeat.
59
- const saved = props.savedStatus ?? props.status
60
- if (props.hasStatus && saved === 'draft')
61
- return { tone: 'blue', word: t('editorStatus.word.notLive'), detail: t('editorStatus.live.draft') }
62
- const s = props.live?.status ?? null
63
- if (s === 'error')
64
- return { tone: 'red', word: t('editorStatus.word.error'), detail: t('editorStatus.live.error'), when: updatedAtLabel.value, error: props.live?.error ?? '' }
65
- // An environment that never generates (dev / static output off) cannot resolve any pending state, so an
66
- // alarming "Outdated" would point at a problem no action here can fix — calm neutral, even when an old
67
- // success row (e.g. a prod run against the same DB) exists.
68
- if (props.live?.generates === false)
69
- return { tone: 'neutral', word: t('editorStatus.word.notBuilt'), detail: t('editorStatus.live.notBuilt') }
70
- if (s === 'success') {
71
- // Saved after the last publish: the file IS live, but it is an older version of this record. Saving no
72
- // longer republishes (ADR-0008), so this is the normal state of a page being worked on — amber, not red.
73
- if (props.live?.pending) {
74
- return { tone: 'amber', word: t('editorStatus.word.outdated'), detail: t('editorStatus.live.outdated'), when: updatedAtLabel.value }
75
- }
76
- // Where the file actually landed — surface local vs S3 in the tooltip ("stored on S3").
77
- const onS3 = (props.live?.target ?? props.live?.driver) === 's3'
78
- return { tone: 'green', word: t('editorStatus.word.live'), detail: t(onS3 ? 'editorStatus.live.liveS3' : 'editorStatus.live.liveLocal'), when: updatedAtLabel.value }
79
- }
80
- // Never published, and a save does not enqueue anything (ADR-0008) — so nothing is in flight and
81
- // nothing will be until someone presses Publish. Reporting progress here would point the user at a
82
- // spinner instead of at the one action that changes it. With `publishOnSave` a save DOES republish, so
83
- // there the in-flight reading is the right one.
84
- if (props.live?.neverPublished && !props.live?.publishOnSave)
85
- return { tone: 'blue', word: t('editorStatus.word.notPublished'), detail: t('editorStatus.live.notPublished') }
86
- return { tone: 'amber', word: t('editorStatus.word.generating'), detail: t('editorStatus.live.pending') }
87
- })
88
- </script>
89
-
90
- <template>
91
- <span class="editor-status-group">
92
- <!-- Polite live region: announces save-state transitions (Saved → Unsaved → Saving…) to screen readers.
93
- Visually hidden; not a tooltip. -->
94
- <span class="editor-status-sr" role="status" aria-live="polite">{{ save.detail }}</span>
95
-
96
- <KestrelUiTooltip side="bottom">
97
- <!-- A focusable status indicator, NOT a <button>: it has no action, and a button whose accessible
98
- name contains "Saved"/"Unsaved" would collide with `getByRole('button', {name:'Save'})`. aria-label
99
- leads with the visible word so it stays part of the accessible name, then adds the detail for AT. -->
100
- <span class="editor-status" tabindex="0" :data-tone="save.tone" :aria-label="`${save.word} — ${save.detail}`">
101
- <span class="editor-status__dot" aria-hidden="true" />
102
- <span class="editor-status__word">{{ save.word }}</span>
103
- </span>
104
- <template #content>
105
- <span class="editor-status-tip">
106
- <strong class="editor-status-tip__detail">{{ save.detail }}</strong>
107
- </span>
108
- </template>
109
- </KestrelUiTooltip>
110
-
111
- <KestrelUiTooltip v-if="live" side="bottom">
112
- <span class="editor-status-live" tabindex="0" :data-tone="live.tone" :aria-label="`${live.word} — ${live.detail}`">
113
- <span class="editor-status-live__dot" aria-hidden="true" />
114
- <span class="editor-status-live__word">{{ live.word }}</span>
115
- </span>
116
- <template #content>
117
- <span class="editor-status-tip">
118
- <strong class="editor-status-tip__detail">{{ live.detail }}</strong>
119
- <span v-if="live.when" class="editor-status-tip__when">{{ live.when }}</span>
120
- <span v-if="live.tone === 'red' && live.error" class="editor-status-tip__err">{{ live.error }}</span>
121
- </span>
122
- </template>
123
- </KestrelUiTooltip>
124
- </span>
125
- </template>
126
-
127
- <style scoped lang="scss">
128
- .editor-status-group {
129
- display: inline-flex;
130
- align-items: center;
131
- gap: var(--space-3);
132
- }
133
-
134
- // Visually-hidden polite status region (mirrors the ui sr-only mixin; admin can't @use a ui-layer scss).
135
- .editor-status-sr {
136
- position: absolute;
137
- width: 1px;
138
- height: 1px;
139
- padding: 0;
140
- margin: -1px;
141
- overflow: hidden;
142
- clip: rect(0, 0, 0, 0);
143
- white-space: nowrap;
144
- border: 0;
145
- }
146
-
147
- // Both lamps are focusable status indicators (tabindex spans, not buttons) so the tooltip opens on hover
148
- // AND keyboard focus, without adding a "Save"-named button that would clash with Save-button selectors.
149
- .editor-status,
150
- .editor-status-live {
151
- display: inline-flex;
152
- align-items: center;
153
- gap: var(--space-2);
154
- cursor: help;
155
-
156
- &__dot {
157
- width: 0.6rem;
158
- height: 0.6rem;
159
- border-radius: var(--radius-full);
160
- background: var(--_tone, var(--color-text-subtle));
161
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--_tone, transparent) 22%, transparent);
162
- flex: none;
163
- }
164
- &__word {
165
- font-size: var(--text-sm);
166
- color: var(--color-text-muted);
167
- white-space: nowrap;
168
- }
169
-
170
- &[data-tone='amber'] {
171
- --_tone: var(--color-warning);
172
- }
173
- &[data-tone='green'] {
174
- --_tone: var(--color-success);
175
- }
176
- &[data-tone='blue'] {
177
- --_tone: var(--color-primary);
178
- }
179
- &[data-tone='red'] {
180
- --_tone: var(--color-danger);
181
- }
182
- &[data-tone='neutral'] {
183
- --_tone: var(--color-text-subtle);
184
- }
185
- &:focus-visible {
186
- outline: 2px solid var(--color-primary);
187
- outline-offset: 2px;
188
- border-radius: var(--radius-sm);
189
- }
190
- }
191
- </style>
192
-
193
- <!-- Not scoped: the tooltip content is teleported to <body> by Reka, where a scoped data-v selector no
194
- longer matches (same reason UiTooltip itself keeps global styles). -->
195
- <style lang="scss">
196
- .editor-status-tip {
197
- display: flex;
198
- flex-direction: column;
199
- gap: var(--space-1);
200
-
201
- &__when {
202
- color: var(--color-text-muted);
203
- font-size: var(--text-xs);
204
- }
205
- &__err {
206
- color: var(--color-danger);
207
- font-size: var(--text-xs);
208
- word-break: break-word;
209
- }
210
- }
211
- </style>
@@ -1,28 +0,0 @@
1
- <script setup lang="ts">
2
- // Fallback body when a collection's `editor` type has no registered body (an extension misconfig or a
3
- // missing plugin). Fails visibly instead of rendering blank.
4
- defineProps<{ editor: string }>()
5
- const { t } = useT()
6
- </script>
7
-
8
- <template>
9
- <div class="editor-unsupported">
10
- <p class="editor-unsupported__note">{{ t('editor.unsupported', { type: editor }) }}</p>
11
- </div>
12
- </template>
13
-
14
- <style lang="scss">
15
- .editor-unsupported {
16
- flex: 1 1 auto;
17
- min-height: 0;
18
-
19
- &__note {
20
- font-size: var(--text-sm);
21
- color: var(--color-text-muted);
22
- padding: var(--space-3) var(--space-4);
23
- border: 1px solid var(--color-border);
24
- border-radius: var(--radius-md);
25
- background: var(--color-surface-2);
26
- }
27
- }
28
- </style>
@@ -1,23 +0,0 @@
1
- <script setup lang="ts">
2
- // The schema-driven default editor body: a plain field list. All state comes from the editor form
3
- // context provided by CollectionEditor; this body only lays it out.
4
- const ctx = useEditorFormContext()
5
- </script>
6
-
7
- <template>
8
- <div class="editor__flat">
9
- <KestrelPageFieldsPane v-bind="ctx.pageFieldsBindings.value" v-on="ctx.pageFieldsHandlers" />
10
- </div>
11
- </template>
12
-
13
- <style lang="scss">
14
- // Flat-collection field list: its own scroll region, so the shell's actions row stays pinned below it.
15
- .editor__flat {
16
- flex: 1 1 auto;
17
- min-height: 0;
18
- overflow-y: auto;
19
- display: flex;
20
- flex-direction: column;
21
- gap: var(--space-4);
22
- }
23
- </style>