@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,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>