@michaelthielemann/kestrel 4.1.2 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (340) hide show
  1. package/README.md +26 -201
  2. package/dist/boot.d.ts +45 -0
  3. package/dist/boot.js +274 -0
  4. package/dist/cast.d.ts +3 -0
  5. package/dist/cast.js +6 -0
  6. package/dist/catalogue.d.ts +11 -0
  7. package/dist/catalogue.js +1 -0
  8. package/dist/cli.d.ts +2 -0
  9. package/dist/cli.js +55 -0
  10. package/dist/context.d.ts +62 -0
  11. package/dist/context.js +61 -0
  12. package/dist/dataflow.d.ts +3 -0
  13. package/dist/dataflow.js +65 -0
  14. package/dist/defineConfig.d.ts +157 -0
  15. package/dist/defineConfig.js +41 -0
  16. package/dist/defineContract.d.ts +12 -0
  17. package/dist/defineContract.js +16 -0
  18. package/dist/defineModule.d.ts +75 -0
  19. package/dist/defineModule.js +9 -0
  20. package/dist/definePipeline.d.ts +7 -0
  21. package/dist/definePipeline.js +11 -0
  22. package/dist/errors.d.ts +26 -0
  23. package/dist/errors.js +48 -0
  24. package/dist/index.d.ts +19 -0
  25. package/dist/index.js +14 -0
  26. package/dist/load.d.ts +7 -0
  27. package/dist/load.js +79 -0
  28. package/dist/logger.d.ts +18 -0
  29. package/dist/logger.js +36 -0
  30. package/dist/registry.d.ts +14 -0
  31. package/dist/registry.js +87 -0
  32. package/dist/result.d.ts +19 -0
  33. package/dist/result.js +19 -0
  34. package/dist/runner.d.ts +29 -0
  35. package/dist/runner.js +133 -0
  36. package/dist/schema.d.ts +7 -0
  37. package/dist/schema.js +142 -0
  38. package/dist/sort.d.ts +2 -0
  39. package/dist/sort.js +50 -0
  40. package/dist/testing/runPipeline.d.ts +9 -0
  41. package/dist/testing/runPipeline.js +22 -0
  42. package/dist/triggers/cron.d.ts +10 -0
  43. package/dist/triggers/cron.js +44 -0
  44. package/dist/triggers/cronMatch.d.ts +10 -0
  45. package/dist/triggers/cronMatch.js +68 -0
  46. package/dist/triggers/http.d.ts +58 -0
  47. package/dist/triggers/http.js +292 -0
  48. package/package.json +25 -151
  49. package/NOTICE +0 -12
  50. package/kestrel.config.ts +0 -28
  51. package/layers/access/nuxt.config.ts +0 -5
  52. package/layers/access/server/middleware/00.ip-allowlist.ts +0 -52
  53. package/layers/access/server/middleware/access-guard.ts +0 -32
  54. package/layers/access/server/types.d.ts +0 -7
  55. package/layers/admin/app/components/AdminAccount.vue +0 -212
  56. package/layers/admin/app/components/AdminNav.vue +0 -44
  57. package/layers/admin/app/components/BlockFields.vue +0 -67
  58. package/layers/admin/app/components/BlockPreview.vue +0 -413
  59. package/layers/admin/app/components/BlockTree.vue +0 -433
  60. package/layers/admin/app/components/BlocksBody.vue +0 -163
  61. package/layers/admin/app/components/CollectionDeleteDialog.vue +0 -38
  62. package/layers/admin/app/components/CollectionEditor.vue +0 -324
  63. package/layers/admin/app/components/CollectionList.vue +0 -429
  64. package/layers/admin/app/components/CollectionListBulkBar.vue +0 -62
  65. package/layers/admin/app/components/CollectionListColumnsPanel.vue +0 -37
  66. package/layers/admin/app/components/CollectionListFilterPanel.vue +0 -145
  67. package/layers/admin/app/components/CollectionListPager.vue +0 -70
  68. package/layers/admin/app/components/CollectionListTable.vue +0 -347
  69. package/layers/admin/app/components/EditorStatus.vue +0 -211
  70. package/layers/admin/app/components/EditorUnsupported.vue +0 -28
  71. package/layers/admin/app/components/FieldsBody.vue +0 -23
  72. package/layers/admin/app/components/LocaleBar.vue +0 -107
  73. package/layers/admin/app/components/PageFields.vue +0 -129
  74. package/layers/admin/app/components/PageFieldsPane.vue +0 -39
  75. package/layers/admin/app/components/SeoFields.vue +0 -210
  76. package/layers/admin/app/components/SingletonEditor.vue +0 -93
  77. package/layers/admin/app/composables/useAuth.ts +0 -68
  78. package/layers/admin/app/composables/useBlockTree.ts +0 -95
  79. package/layers/admin/app/composables/useBlocks.ts +0 -7
  80. package/layers/admin/app/composables/useCachedResource.ts +0 -35
  81. package/layers/admin/app/composables/useCollectionOps.ts +0 -102
  82. package/layers/admin/app/composables/useCollections.ts +0 -6
  83. package/layers/admin/app/composables/useContentLocales.ts +0 -23
  84. package/layers/admin/app/composables/useEditForm.ts +0 -402
  85. package/layers/admin/app/composables/useLinkResolver.ts +0 -40
  86. package/layers/admin/app/composables/useListBatchActions.ts +0 -78
  87. package/layers/admin/app/composables/useListColumns.ts +0 -53
  88. package/layers/admin/app/composables/useListFilterDraft.ts +0 -129
  89. package/layers/admin/app/composables/useListRows.ts +0 -75
  90. package/layers/admin/app/composables/useListSelection.ts +0 -28
  91. package/layers/admin/app/composables/useListUrlState.ts +0 -53
  92. package/layers/admin/app/composables/usePendingPublishPoll.ts +0 -15
  93. package/layers/admin/app/composables/usePreviewViewport.ts +0 -30
  94. package/layers/admin/app/composables/usePublishStatus.ts +0 -107
  95. package/layers/admin/app/composables/useRailCollapsed.ts +0 -14
  96. package/layers/admin/app/composables/useTheme.ts +0 -36
  97. package/layers/admin/app/composables/useToolbarPanel.ts +0 -38
  98. package/layers/admin/app/composables/useUnsavedGuard.ts +0 -22
  99. package/layers/admin/app/layouts/admin.vue +0 -266
  100. package/layers/admin/app/middleware/admin-auth.ts +0 -4
  101. package/layers/admin/app/pages/admin/[collection]/[id].vue +0 -203
  102. package/layers/admin/app/pages/admin/[collection]/index.vue +0 -62
  103. package/layers/admin/app/pages/admin/index.vue +0 -118
  104. package/layers/admin/app/pages/admin/login.vue +0 -68
  105. package/layers/admin/app/pages/admin/media.vue +0 -28
  106. package/layers/admin/app/pages/admin/references.vue +0 -146
  107. package/layers/admin/app/plugins/admin-style-guard.client.ts +0 -76
  108. package/layers/admin/app/plugins/reauth.client.ts +0 -20
  109. package/layers/admin/app/utils/admin-style-guard.ts +0 -20
  110. package/layers/admin/app/utils/block-tree.ts +0 -214
  111. package/layers/admin/app/utils/collection-ops.ts +0 -69
  112. package/layers/admin/app/utils/dead-refs.ts +0 -30
  113. package/layers/admin/app/utils/edit-form.ts +0 -267
  114. package/layers/admin/app/utils/editor-expose.ts +0 -36
  115. package/layers/admin/app/utils/editor-form-context.ts +0 -82
  116. package/layers/admin/app/utils/editor-registry.ts +0 -20
  117. package/layers/admin/app/utils/field-keys.ts +0 -11
  118. package/layers/admin/app/utils/list-cell.ts +0 -46
  119. package/layers/admin/app/utils/list-columns.ts +0 -142
  120. package/layers/admin/app/utils/list-query.ts +0 -96
  121. package/layers/admin/app/utils/nav-active.ts +0 -9
  122. package/layers/admin/app/utils/populate-blocks.ts +0 -288
  123. package/layers/admin/app/utils/preview-channel.ts +0 -62
  124. package/layers/admin/app/utils/preview-viewport.ts +0 -80
  125. package/layers/admin/app/utils/reauth.ts +0 -67
  126. package/layers/admin/app/utils/record-title.ts +0 -15
  127. package/layers/admin/app/utils/register-builtin-editors.ts +0 -9
  128. package/layers/admin/app/utils/safe-redirect.ts +0 -12
  129. package/layers/admin/nuxt.config.ts +0 -20
  130. package/layers/auth/nuxt.config.ts +0 -1
  131. package/layers/auth/server/plugins/01.register-auth-pipelines.ts +0 -7
  132. package/layers/collections/nuxt.config.ts +0 -1
  133. package/layers/collections/server/plugins/01.register.ts +0 -29
  134. package/layers/collections/server/plugins/02.register-relation-populate.ts +0 -18
  135. package/layers/core/app/composables/layouts.ts +0 -5
  136. package/layers/core/app/utils/field-factories.ts +0 -129
  137. package/layers/core/app/utils/layouts.ts +0 -38
  138. package/layers/core/modules/auto-discovery/extract-block.ts +0 -179
  139. package/layers/core/modules/auto-discovery/index.ts +0 -112
  140. package/layers/core/modules/auto-discovery/package-registry.ts +0 -19
  141. package/layers/core/modules/auto-discovery/scan.ts +0 -170
  142. package/layers/core/modules/auto-discovery/virtual.d.ts +0 -20
  143. package/layers/core/modules/component-namespace/index.ts +0 -44
  144. package/layers/core/modules/component-namespace/shared.ts +0 -24
  145. package/layers/core/modules/kestrel/app-shell.ts +0 -55
  146. package/layers/core/modules/kestrel/auto-imports.ts +0 -44
  147. package/layers/core/modules/kestrel/index.ts +0 -144
  148. package/layers/core/modules/plugin-order/index.ts +0 -69
  149. package/layers/core/modules/plugin-order/plugin-order.ts +0 -273
  150. package/layers/core/nuxt.config.ts +0 -10
  151. package/layers/core/server/api/[...path].ts +0 -98
  152. package/layers/core/server/plugins/00.config.ts +0 -12
  153. package/layers/core/server/plugins/00.migrate.ts +0 -12
  154. package/layers/core/server/plugins/01.register-introspection-pipeline.ts +0 -5
  155. package/layers/core/server/plugins/01.register-openapi-pipeline.ts +0 -5
  156. package/layers/core/server/plugins/01.register-tooling-pipelines.ts +0 -5
  157. package/layers/core/server/plugins/02.schema-sync.ts +0 -58
  158. package/layers/core/server/plugins/04.outbox-worker.ts +0 -31
  159. package/layers/core/server/plugins/05.reindex-refs.ts +0 -5
  160. package/layers/core/server/routes/__kestrel/dashboard.get.ts +0 -29
  161. package/layers/core/server/tasks/db/migrate-module.ts +0 -83
  162. package/layers/core/server/tasks/db/migrate-revisions.ts +0 -20
  163. package/layers/core/server/tasks/db/migrate.ts +0 -50
  164. package/layers/core/server/utils/server-config.ts +0 -81
  165. package/layers/fields/nuxt.config.ts +0 -1
  166. package/layers/fields/server/plugins/01.register-blocks-pipeline.ts +0 -7
  167. package/layers/fields/server/plugins/01.register-field-populate.ts +0 -11
  168. package/layers/media/app/components/KestrelImg.vue +0 -94
  169. package/layers/media/app/components/MediaConflictDialog.vue +0 -35
  170. package/layers/media/app/components/MediaDeleteDialog.vue +0 -52
  171. package/layers/media/app/components/MediaGrid.vue +0 -113
  172. package/layers/media/app/components/MediaImage.vue +0 -70
  173. package/layers/media/app/components/MediaLibrary.vue +0 -456
  174. package/layers/media/app/components/MediaNewFolderDialog.vue +0 -32
  175. package/layers/media/app/components/MediaPathBar.vue +0 -163
  176. package/layers/media/app/components/MediaPicker.vue +0 -19
  177. package/layers/media/app/components/MediaRenameDialog.vue +0 -32
  178. package/layers/media/app/components/MediaTable.vue +0 -75
  179. package/layers/media/app/components/MediaThumb.vue +0 -25
  180. package/layers/media/app/components/MediaToolbar.vue +0 -90
  181. package/layers/media/app/components/MediaUploadDialog.vue +0 -87
  182. package/layers/media/app/components/MediaViewer.vue +0 -137
  183. package/layers/media/app/components/field/Media.vue +0 -263
  184. package/layers/media/app/composables/useBlurUp.ts +0 -55
  185. package/layers/media/app/composables/useMediaClipboard.ts +0 -14
  186. package/layers/media/app/composables/useMediaContextMenu.ts +0 -56
  187. package/layers/media/app/composables/useMediaDnd.ts +0 -56
  188. package/layers/media/app/composables/useMediaLibrary.ts +0 -152
  189. package/layers/media/app/composables/useMediaOps.ts +0 -50
  190. package/layers/media/app/composables/useMediaRelocate.ts +0 -39
  191. package/layers/media/app/composables/useMediaResolver.ts +0 -50
  192. package/layers/media/app/composables/useMediaUpload.ts +0 -93
  193. package/layers/media/app/composables/useMediaVariant.ts +0 -34
  194. package/layers/media/app/plugins/field-media.client.ts +0 -6
  195. package/layers/media/app/utils/ai-disclosure.ts +0 -19
  196. package/layers/media/app/utils/dnd.ts +0 -69
  197. package/layers/media/app/utils/library.ts +0 -81
  198. package/layers/media/app/utils/ops.ts +0 -60
  199. package/layers/media/app/utils/picture-model.ts +0 -125
  200. package/layers/media/app/utils/thumbhash.ts +0 -19
  201. package/layers/media/app/utils/variant-name.ts +0 -20
  202. package/layers/media/nuxt.config.ts +0 -7
  203. package/layers/media/server/middleware/ondemand-variants.ts +0 -38
  204. package/layers/media/server/plugins/01.register-media-pipelines.ts +0 -7
  205. package/layers/media/server/plugins/02.register-media.ts +0 -22
  206. package/layers/media/server/plugins/04.variant-capture.ts +0 -27
  207. package/layers/media/server/plugins/05.media-cleanup.ts +0 -5
  208. package/layers/media/server/tasks/media/backfill.ts +0 -17
  209. package/layers/public/app/app.vue +0 -5
  210. package/layers/public/app/components/BlockRenderer.vue +0 -56
  211. package/layers/public/app/components/KestrelLink.vue +0 -26
  212. package/layers/public/app/components/KestrelPreviewBridge.vue +0 -133
  213. package/layers/public/app/composables/public-page.ts +0 -24
  214. package/layers/public/app/error.vue +0 -12
  215. package/layers/public/app/layouts/default.vue +0 -5
  216. package/layers/public/app/pages/[...slug].vue +0 -239
  217. package/layers/public/app/pages/__kestrel/preview.vue +0 -46
  218. package/layers/public/app/utils/block-edit-context.ts +0 -16
  219. package/layers/public/app/utils/json-ld.ts +0 -139
  220. package/layers/public/app/utils/link-href.ts +0 -46
  221. package/layers/public/app/utils/page-head.ts +0 -84
  222. package/layers/public/app/utils/page-layout.ts +0 -18
  223. package/layers/public/app/utils/preview-protocol.ts +0 -117
  224. package/layers/public/app/utils/public-route.ts +0 -46
  225. package/layers/public/app/utils/site-head.ts +0 -43
  226. package/layers/public/modules/deploy-output/index.ts +0 -80
  227. package/layers/public/modules/prerender-routes/discover.ts +0 -37
  228. package/layers/public/modules/prerender-routes/index.ts +0 -88
  229. package/layers/public/modules/prerender-routes/routes.ts +0 -16
  230. package/layers/public/modules/prune-media/index.ts +0 -48
  231. package/layers/public/modules/prune-media/prune-media.ts +0 -81
  232. package/layers/public/nuxt.config.ts +0 -14
  233. package/layers/public/server/delivery-live/serve.ts +0 -111
  234. package/layers/public/server/middleware/delivery-live-catchall.ts +0 -7
  235. package/layers/public/server/pipelines/index.ts +0 -23
  236. package/layers/public/server/plugins/00.ensure-snapshot-triggers.ts +0 -14
  237. package/layers/public/server/plugins/01.register-public-pipelines.ts +0 -7
  238. package/layers/public/server/plugins/02.register-links.ts +0 -16
  239. package/layers/public/server/plugins/03.redirects.ts +0 -55
  240. package/layers/public/server/plugins/05.plan-publish.ts +0 -5
  241. package/layers/public/server/plugins/zz.publish.ts +0 -127
  242. package/layers/public/server/routes/llms-full.txt.get.ts +0 -99
  243. package/layers/public/server/routes/llms.txt.get.ts +0 -66
  244. package/layers/public/server/routes/redirects.json.get.ts +0 -58
  245. package/layers/public/server/routes/robots.txt.get.ts +0 -11
  246. package/layers/public/server/routes/sitemap.xml.get.ts +0 -79
  247. package/layers/public/server/tasks/publish/run.ts +0 -32
  248. package/layers/public/server/utils/publish/render-live.ts +0 -26
  249. package/layers/ui/app/assets/scss/_base.scss +0 -16
  250. package/layers/ui/app/assets/scss/_datepicker.scss +0 -82
  251. package/layers/ui/app/assets/scss/_mixins.scss +0 -50
  252. package/layers/ui/app/assets/scss/_reset.scss +0 -65
  253. package/layers/ui/app/assets/scss/_tokens.scss +0 -183
  254. package/layers/ui/app/assets/scss/main.scss +0 -4
  255. package/layers/ui/app/components/field/Boolean.vue +0 -30
  256. package/layers/ui/app/components/field/Choice.vue +0 -73
  257. package/layers/ui/app/components/field/Datetime.vue +0 -98
  258. package/layers/ui/app/components/field/DeadRefNote.vue +0 -28
  259. package/layers/ui/app/components/field/Json.vue +0 -62
  260. package/layers/ui/app/components/field/Layout.vue +0 -138
  261. package/layers/ui/app/components/field/Link.vue +0 -112
  262. package/layers/ui/app/components/field/LinkInternalPicker.vue +0 -95
  263. package/layers/ui/app/components/field/Number.vue +0 -30
  264. package/layers/ui/app/components/field/Relation.vue +0 -49
  265. package/layers/ui/app/components/field/Renderer.vue +0 -30
  266. package/layers/ui/app/components/field/Repeater.vue +0 -460
  267. package/layers/ui/app/components/field/Richtext.vue +0 -26
  268. package/layers/ui/app/components/field/Slug.vue +0 -65
  269. package/layers/ui/app/components/field/Text.vue +0 -35
  270. package/layers/ui/app/components/field/Unsupported.vue +0 -34
  271. package/layers/ui/app/components/ui/Alert.vue +0 -50
  272. package/layers/ui/app/components/ui/Brand.vue +0 -22
  273. package/layers/ui/app/components/ui/Button.vue +0 -116
  274. package/layers/ui/app/components/ui/ButtonGroup.vue +0 -96
  275. package/layers/ui/app/components/ui/Checkbox.vue +0 -53
  276. package/layers/ui/app/components/ui/CheckboxGroup.vue +0 -51
  277. package/layers/ui/app/components/ui/Combobox.vue +0 -345
  278. package/layers/ui/app/components/ui/DatePicker.vue +0 -92
  279. package/layers/ui/app/components/ui/DateRangePicker.vue +0 -100
  280. package/layers/ui/app/components/ui/Dialog.vue +0 -107
  281. package/layers/ui/app/components/ui/EmptyState.vue +0 -46
  282. package/layers/ui/app/components/ui/Field.vue +0 -58
  283. package/layers/ui/app/components/ui/Fieldset.vue +0 -65
  284. package/layers/ui/app/components/ui/Icon.vue +0 -44
  285. package/layers/ui/app/components/ui/Menu.vue +0 -54
  286. package/layers/ui/app/components/ui/NumberInput.vue +0 -175
  287. package/layers/ui/app/components/ui/Richtext.vue +0 -141
  288. package/layers/ui/app/components/ui/RichtextToolbar.vue +0 -251
  289. package/layers/ui/app/components/ui/Select.vue +0 -52
  290. package/layers/ui/app/components/ui/TextInput.vue +0 -111
  291. package/layers/ui/app/components/ui/Textarea.vue +0 -39
  292. package/layers/ui/app/components/ui/TimeInput.vue +0 -18
  293. package/layers/ui/app/components/ui/Toasts.vue +0 -103
  294. package/layers/ui/app/components/ui/Tooltip.vue +0 -45
  295. package/layers/ui/app/composables/useAdminLang.ts +0 -8
  296. package/layers/ui/app/composables/useDragReorder.ts +0 -41
  297. package/layers/ui/app/composables/useEchoGuard.ts +0 -28
  298. package/layers/ui/app/composables/useLinkField.ts +0 -70
  299. package/layers/ui/app/composables/useRecordOptions.ts +0 -84
  300. package/layers/ui/app/composables/useRepeater.ts +0 -99
  301. package/layers/ui/app/composables/useT.ts +0 -32
  302. package/layers/ui/app/composables/useToast.ts +0 -46
  303. package/layers/ui/app/i18n/de.ts +0 -432
  304. package/layers/ui/app/i18n/en.ts +0 -434
  305. package/layers/ui/app/utils/boolean-field.ts +0 -10
  306. package/layers/ui/app/utils/field-component.ts +0 -22
  307. package/layers/ui/app/utils/field-empty.ts +0 -21
  308. package/layers/ui/app/utils/field-registry.ts +0 -36
  309. package/layers/ui/app/utils/humanize.ts +0 -14
  310. package/layers/ui/app/utils/icons.ts +0 -143
  311. package/layers/ui/app/utils/iso-date.ts +0 -18
  312. package/layers/ui/app/utils/localized.ts +0 -13
  313. package/layers/ui/app/utils/record-context.ts +0 -15
  314. package/layers/ui/app/utils/reorder.ts +0 -8
  315. package/layers/ui/app/utils/richtext-preserve-class.ts +0 -31
  316. package/layers/ui/app/utils/useFieldA11y.ts +0 -40
  317. package/layers/ui/nuxt.config.ts +0 -9
  318. package/nuxt.config.ts +0 -108
  319. package/scripts/consumer-template-ci.mjs +0 -160
  320. package/scripts/copy-create-payload.mjs +0 -49
  321. package/scripts/coverage-all.mjs +0 -163
  322. package/scripts/dashboard.mjs +0 -89
  323. package/scripts/hash-password.mjs +0 -29
  324. package/scripts/kestrel.mjs +0 -240
  325. package/scripts/lib/cli.mjs +0 -129
  326. package/scripts/lib/coverage-model.mjs +0 -51
  327. package/scripts/lib/password.mjs +0 -19
  328. package/scripts/lib/scaffold.mjs +0 -205
  329. package/scripts/port-graph.mjs +0 -350
  330. package/scripts/typecheck.mjs +0 -62
  331. package/templates/starter/README.md +0 -65
  332. package/templates/starter/_env.example +0 -17
  333. package/templates/starter/_gitignore +0 -26
  334. package/templates/starter/_package.json +0 -22
  335. package/templates/starter/app/app.vue +0 -7
  336. package/templates/starter/app/blocks/Prose.vue +0 -13
  337. package/templates/starter/app/layouts/default.vue +0 -6
  338. package/templates/starter/nuxt.config.ts +0 -23
  339. package/templates/starter/pnpm-workspace.yaml +0 -8
  340. package/templates/starter/tsconfig.json +0 -3
@@ -1,429 +0,0 @@
1
- <script setup lang="ts">
2
- import type { SerializedCollection } from '@michaelthielemann/kestrel-core'
3
- import { resolveLocalized } from '../../../ui/app/utils/localized'
4
- import { bulkCustomActions, recordCustomActions } from '../utils/collection-ops'
5
-
6
- // `locale` is set only for a translatable collection: it filters the list to that content locale and
7
- // shows a locale switcher. New records / row links carry it so they open in the same locale.
8
- const props = defineProps<{ schema: SerializedCollection; locale?: string }>()
9
-
10
- const { t, lang } = useT()
11
- const { locales } = useContentLocales()
12
-
13
- const collection = computed(() => props.schema.name)
14
- const label = computed(() => resolveLocalized(props.schema.label?.singular, lang.value) ?? props.schema.name)
15
- // Prefer the collection's complete per-locale "create" phrase; fall back to the generic template.
16
- const newLabel = computed(() => resolveLocalized(props.schema.label?.new, lang.value) ?? t('common.new', { label: label.value }))
17
- const localeQuery = computed(() => (props.locale ? `?locale=${props.locale}` : ''))
18
-
19
- // Configurable columns (persisted per-collection). The select + row-action columns are CHROME — fixed
20
- // cells rendered outside this model, never sortable/filterable/hideable/persisted.
21
- const { available, visibleColumns, visibleKeys, toggle: toggleColumn, reset: resetColumns, isDefault: columnsAreDefault } =
22
- useListColumns(toRef(props, 'schema'))
23
- const filterableColumns = computed(() => available.value.filter((c) => c.filterable))
24
-
25
- // The URL is the single source of truth for committed list state. `sort`/`page`/`perPage`/`filter` are
26
- // derived (read-only) computeds off `route.query`; `effectiveQuery` is the sanitized `$fetch` query; the
27
- // setters navigate (page → push so Back walks pages, the rest → replace). The composable also owns the
28
- // per-page cookie (a density DEFAULT that seeds perPage when the URL omits it).
29
- const { sort, page, perPage, filter, effectiveQuery, setSort, setPage, clampPage, setPerPage, setFilter } =
30
- useListUrlState(available)
31
-
32
- // `filter` (above, from the URL) = the committed clauses that drive the query; the panel's pending edits
33
- // live in `draft` inside the composable, which also owns the debounce and the chip model.
34
- const filterDraft = useListFilterDraft({ available, filterableColumns, filter, setFilter })
35
- const activeFilters = filterDraft.activeFilters
36
-
37
- const { rows, total, quarantinedCount, error, totalPages, fetchRows } = useListRows({
38
- collection,
39
- effectiveQuery,
40
- locale: () => props.locale,
41
- page,
42
- perPage,
43
- clampPage,
44
- // Selection is PAGE-SCOPED: any (re)fetch — page, filter, sort, per-page — replaces the visible rows, so
45
- // the previous selection no longer maps to what's on screen. (A locale switch remounts the whole
46
- // component, so that case clears too.)
47
- onLoaded: () => clearSelection(),
48
- })
49
-
50
- const { selected, allSelected, headerIndeterminate, toggleRow, toggleAll, clear: clearSelection } =
51
- useListSelection(rows)
52
-
53
- // Row action = 1 id, bulk bar = N ids; the server does the batching.
54
- const { busy: opsBusy, error: opsError, deleteOpen, deleteReport, askDelete, confirmDelete, duplicate: onDuplicate, setStatus, runAction } =
55
- useListBatchActions(collection, fetchRows)
56
-
57
- // Schema-driven actions beyond the built-in delete/duplicate/publish: a consumer's `definePipeline` shows
58
- // up here without any UI code.
59
- const bulkActions = computed(() => bulkCustomActions(props.schema.actions ?? []))
60
- const rowActions = computed(() => recordCustomActions(props.schema.actions ?? []))
61
-
62
- // Toolbar popovers (filter / columns), including the outside-click and Escape-restores-focus behaviour.
63
- const { container: toolsRef, open: openPanel, toggle: togglePanel } = useToolbarPanel<'filter' | 'columns'>()
64
-
65
- // Polite status announced after every (re)fetch so filter/sort/page changes report their result count
66
- // to screen readers — WCAG 4.1.3 Status Messages.
67
- const resultsLabel = computed(() => t(total.value === 1 ? 'list.result' : 'list.results', { total: total.value }))
68
- // Quarantined rows failed their select schema and would otherwise be silent data loss — a chip in
69
- // the header surfaces the count whenever it's non-zero.
70
- const quarantinedLabel = computed(() => t(quarantinedCount.value === 1 ? 'list.quarantinedOne' : 'list.quarantinedCount', { n: quarantinedCount.value }))
71
- // The ONE permanent live region. A live region only announces MUTATIONS to a node already in the tree, so
72
- // the selection count is folded in here (mutating this region's text) rather than toggling a second
73
- // role="status" node into the DOM — inserting an already-populated live node is typically not announced,
74
- // which would silence the first selection. Selection takes precedence; with none, it reports the count.
75
- const srStatus = computed(() => (selected.size ? t('list.selected', { n: selected.size }) : resultsLabel.value))
76
-
77
- // Exactly ONE refetch per navigation: the effective (sanitized) query is stringified so an identical or
78
- // junk-only URL change never refetches, and a single change fires a single fetch. Every URL-driven refetch
79
- // clears the page-scoped selection inside `fetchRows`.
80
- watch(() => JSON.stringify(effectiveQuery.value), fetchRows)
81
- // The only host (pages/admin/[collection]/index.vue) keys this component on collection + locale, so a
82
- // collection- or locale-change fully remounts it (resetting committed filter/draft/page/selection from the
83
- // clean target URL) while a sort/page/perPage/filter query change does NOT remount — the derived computeds
84
- // recompute in place, keeping the filter panel open and focus intact. The initial load is the
85
- // `await fetchRows()` below; subsequent loads flow through the effective-query watch above.
86
- await fetchRows()
87
- </script>
88
-
89
- <template>
90
- <div class="list">
91
- <div class="list__bar">
92
- <div v-if="locale" class="list__locales" role="group" :aria-label="t('a11y.contentLocale')">
93
- <NuxtLink
94
- v-for="loc in locales"
95
- :key="loc"
96
- :to="`/admin/${collection}?locale=${loc}`"
97
- class="list__locale"
98
- :class="{ 'list__locale--active': loc === locale }"
99
- :aria-current="loc === locale ? 'true' : undefined"
100
- >{{ loc.toUpperCase() }}</NuxtLink>
101
- </div>
102
-
103
- <div ref="toolsRef" class="list__tools">
104
- <div class="list__tool">
105
- <KestrelUiButton
106
- type="button"
107
- size="sm"
108
- icon="filter"
109
- aria-controls="list-filter-panel"
110
- :aria-expanded="openPanel === 'filter'"
111
- class="list__tool-btn"
112
- @click="togglePanel('filter', $event)"
113
- >
114
- {{ t('list.filter') }}<span v-if="activeFilters.length" class="list__tool-count">{{ activeFilters.length }}</span>
115
- </KestrelUiButton>
116
- <KestrelCollectionListFilterPanel
117
- v-if="openPanel === 'filter'"
118
- :columns="filterableColumns"
119
- :filter="filterDraft"
120
- :has-active="activeFilters.length > 0"
121
- />
122
- </div>
123
-
124
- <div class="list__tool">
125
- <KestrelUiButton
126
- type="button"
127
- size="sm"
128
- icon="columns"
129
- aria-controls="list-columns-panel"
130
- :aria-expanded="openPanel === 'columns'"
131
- class="list__tool-btn"
132
- @click="togglePanel('columns', $event)"
133
- >{{ t('list.columns') }}</KestrelUiButton>
134
- <KestrelCollectionListColumnsPanel
135
- v-if="openPanel === 'columns'"
136
- :columns="available"
137
- :visible-keys="visibleKeys"
138
- :is-default="columnsAreDefault"
139
- @toggle="toggleColumn"
140
- @reset="resetColumns"
141
- />
142
- </div>
143
- </div>
144
-
145
- <span v-if="quarantinedCount > 0" class="list__quarantine-chip">
146
- <KestrelUiIcon name="triangle-alert" :size="14" />
147
- {{ quarantinedLabel }}
148
- </span>
149
-
150
- <NuxtLink :to="`/admin/${collection}/new${localeQuery}`" class="list__new">{{ newLabel }}</NuxtLink>
151
- </div>
152
-
153
- <KestrelCollectionListBulkBar
154
- v-if="selected.size"
155
- :count="selected.size"
156
- :has-status="!!schema.status"
157
- :busy="opsBusy"
158
- :actions="bulkActions"
159
- @set-status="(status) => setStatus([...selected], status)"
160
- @delete="askDelete([...selected])"
161
- @run-action="(action) => runAction(action, [...selected])"
162
- @clear="clearSelection"
163
- />
164
-
165
- <div v-if="activeFilters.length" class="list__chips">
166
- <span v-for="f in activeFilters" :key="f.key" class="list__chip">
167
- <span class="list__chip-label">{{ f.label }} {{ f.opLabel }}:</span> {{ f.display }}
168
- <button type="button" class="list__chip-x" :aria-label="t('list.removeFilter', { name: f.label })" @click="filterDraft.clearFilter(f.key)">
169
- <KestrelUiIcon name="x" :size="12" />
170
- </button>
171
- </span>
172
- </div>
173
-
174
- <KestrelUiAlert v-if="error" variant="error" class="list__error">
175
- {{ error }}
176
- <KestrelUiButton type="button" variant="secondary" size="sm" class="list__retry" @click="fetchRows">{{ t('common.retry') }}</KestrelUiButton>
177
- </KestrelUiAlert>
178
-
179
- <!-- The one scroll region: the table + its alternate empty-states scroll here while the bar, chips,
180
- error and pager stay fixed (see the admin layout's scroll-ownership convention). -->
181
- <div class="list__scroll">
182
- <KestrelCollectionListTable
183
- :rows="rows"
184
- :columns="visibleColumns"
185
- :collection="collection"
186
- :locale-query="localeQuery"
187
- :sort="sort"
188
- :busy="opsBusy"
189
- :selected="selected"
190
- :all-selected="allSelected"
191
- :header-indeterminate="headerIndeterminate"
192
- :actions="rowActions"
193
- @sort="setSort"
194
- @toggle-row="toggleRow"
195
- @toggle-all="toggleAll"
196
- @duplicate="onDuplicate"
197
- @delete="askDelete"
198
- @run-action="(action, id) => runAction(action, [id])"
199
- />
200
-
201
- <KestrelUiEmptyState
202
- v-if="!rows.length && !error && activeFilters.length"
203
- icon="filter"
204
- :title="t('list.noMatch.title')"
205
- :description="t('list.noMatch.desc', { name: label })"
206
- >
207
- <template #action>
208
- <button type="button" class="list__new" @click="filterDraft.clearAll">{{ t('list.filterClearAll') }}</button>
209
- </template>
210
- </KestrelUiEmptyState>
211
- <KestrelUiEmptyState
212
- v-else-if="!rows.length && !error"
213
- icon="file-text"
214
- :title="t('list.empty.title')"
215
- :description="t('list.empty.desc', { name: label })"
216
- >
217
- <template #action>
218
- <NuxtLink :to="`/admin/${collection}/new${localeQuery}`" class="list__new">{{ newLabel }}</NuxtLink>
219
- </template>
220
- </KestrelUiEmptyState>
221
- </div>
222
-
223
- <p class="list__sr-status" role="status" aria-live="polite">{{ srStatus }}</p>
224
-
225
- <KestrelCollectionListPager
226
- :page="page"
227
- :total-pages="totalPages"
228
- :total="total"
229
- :per-page="perPage"
230
- @update:page="setPage"
231
- @update:per-page="setPerPage"
232
- />
233
-
234
- <KestrelCollectionDeleteDialog
235
- :open="deleteOpen"
236
- :report="deleteReport"
237
- :busy="opsBusy"
238
- :error="opsError"
239
- @update:open="deleteOpen = $event"
240
- @confirm="confirmDelete"
241
- />
242
- </div>
243
- </template>
244
-
245
- <style lang="scss">
246
- .list__retry {
247
- margin-inline-start: var(--space-3);
248
- }
249
-
250
- // Visually-hidden polite status region (mirrors the ui sr-only mixin; admin can't @use a ui-layer scss).
251
- .list__sr-status {
252
- position: absolute;
253
- width: 1px;
254
- height: 1px;
255
- padding: 0;
256
- margin: -1px;
257
- overflow: hidden;
258
- clip: rect(0, 0, 0, 0);
259
- white-space: nowrap;
260
- border: 0;
261
- }
262
-
263
- // Fills the height handed down and stays fixed itself; scroll is delegated to .list__scroll. Overflow is
264
- // left VISIBLE (not a scroll container) so the inline Filter/Columns popovers are never clipped.
265
- .list {
266
- // Frozen-column token: the actions column's sticky `left` must equal the select column's width
267
- // (1.25rem checkbox + 2×var(--space-3) cell padding). Read by CollectionListTable's frozen columns —
268
- // keep in sync if the checkbox size or padding change.
269
- --list-col-select: 2.75rem;
270
- display: flex;
271
- flex-direction: column;
272
- gap: var(--space-4);
273
- flex: 1 1 auto;
274
- min-height: 0;
275
-
276
- // Fixed chrome around the one scroll region.
277
- &__bar,
278
- &__bulkbar,
279
- &__chips,
280
- &__error,
281
- &__pager {
282
- flex: 0 0 auto;
283
- }
284
-
285
- // The only scroller: the table (+ its empty-states) scroll here; the bar/chips/error/pager stay put.
286
- &__scroll {
287
- flex: 1 1 auto;
288
- min-height: 0;
289
- overflow: auto;
290
- }
291
-
292
- &__bar {
293
- display: flex;
294
- align-items: center;
295
- gap: var(--space-3);
296
- }
297
- &__locales {
298
- display: flex;
299
- gap: var(--space-1);
300
- }
301
- &__locale {
302
- padding: var(--space-1) var(--space-2);
303
- border: 1px solid var(--color-border);
304
- border-radius: var(--radius-sm);
305
- font-size: var(--text-sm);
306
- font-weight: var(--weight-medium);
307
- color: var(--color-text-muted);
308
- text-decoration: none;
309
-
310
- &--active {
311
- background: var(--color-surface);
312
- color: var(--color-text);
313
- }
314
- }
315
-
316
- // Filter / Columns toolbar. The panels themselves live in CollectionListFilterPanel /
317
- // CollectionListColumnsPanel; the shared popover chrome below styles both.
318
- &__tools {
319
- display: flex;
320
- align-items: center;
321
- gap: var(--space-2);
322
- }
323
- &__tool {
324
- position: relative;
325
- }
326
- &__tool-count {
327
- margin-left: var(--space-1);
328
- padding: 0 var(--space-1);
329
- border-radius: var(--radius-sm);
330
- background: var(--color-primary-soft);
331
- color: var(--color-primary);
332
- font-size: var(--text-xs);
333
- font-weight: var(--weight-medium);
334
- }
335
- &__panel {
336
- position: absolute;
337
- z-index: 20;
338
- top: calc(100% + var(--space-1));
339
- left: 0;
340
- min-width: 16rem;
341
- max-height: 60vh;
342
- overflow-y: auto;
343
- display: flex;
344
- flex-direction: column;
345
- gap: var(--space-2);
346
- padding: var(--space-3);
347
- border: 1px solid var(--color-border);
348
- border-radius: var(--radius-md);
349
- background: var(--color-surface);
350
- box-shadow: var(--shadow-md, 0 6px 24px rgb(0 0 0 / 12%));
351
- }
352
- &__panel-empty {
353
- margin: 0;
354
- color: var(--color-text-muted);
355
- font-size: var(--text-sm);
356
- }
357
- &__panel-action {
358
- align-self: flex-start;
359
- margin-top: var(--space-1);
360
- padding: 0;
361
- border: 0;
362
- background: none;
363
- color: var(--color-primary);
364
- font: inherit;
365
- font-size: var(--text-sm);
366
- cursor: pointer;
367
- }
368
-
369
- // Quarantine count chip — icon + text (not color alone, WCAG 1.4.1) so it reads even without color.
370
- &__quarantine-chip {
371
- display: inline-flex;
372
- align-items: center;
373
- gap: var(--space-1);
374
- padding: var(--space-1) var(--space-2);
375
- border-radius: var(--radius-sm);
376
- background: var(--color-danger-soft, var(--color-surface));
377
- color: var(--color-danger);
378
- font-size: var(--text-sm);
379
- font-weight: var(--weight-medium);
380
- }
381
-
382
- &__new {
383
- margin-left: auto;
384
- padding: var(--space-2) var(--space-4);
385
- border-radius: var(--radius-md);
386
- background: var(--color-primary);
387
- color: var(--color-on-primary);
388
- text-decoration: none;
389
- font-size: var(--text-sm);
390
- font-weight: var(--weight-medium);
391
-
392
- &:hover {
393
- background: var(--color-primary-hover);
394
- }
395
- }
396
-
397
- // Active-filter chips
398
- &__chips {
399
- display: flex;
400
- flex-wrap: wrap;
401
- gap: var(--space-2);
402
- }
403
- &__chip {
404
- display: inline-flex;
405
- align-items: center;
406
- gap: var(--space-1);
407
- padding: var(--space-1) var(--space-2);
408
- border: 1px solid var(--color-border);
409
- border-radius: var(--radius-sm);
410
- background: var(--color-surface);
411
- font-size: var(--text-sm);
412
- }
413
- &__chip-label {
414
- color: var(--color-text-muted);
415
- }
416
- &__chip-x {
417
- display: inline-flex;
418
- padding: 0;
419
- border: 0;
420
- background: none;
421
- color: var(--color-text-muted);
422
- cursor: pointer;
423
-
424
- &:hover {
425
- color: var(--color-text);
426
- }
427
- }
428
- }
429
- </style>
@@ -1,62 +0,0 @@
1
- <script setup lang="ts">
2
- // Chrome (flex: 0 0 auto) that appears when ≥1 row is selected, shrinking the one scroll region rather
3
- // than overlaying it. Publish/Unpublish are schema-driven (only when the collection has a status).
4
- // NOT a live region: the selection count is announced through the list's permanent .list__sr-status
5
- // region; a v-if-toggled role="status" here would double up and stay silent on first show.
6
- import { resolveLocalized } from '../../../ui/app/utils/localized'
7
- import type { SerializedAction } from '@michaelthielemann/kestrel-core'
8
-
9
- const props = defineProps<{ count: number; hasStatus: boolean; busy: boolean; actions?: SerializedAction[] }>()
10
- const emit = defineEmits<{ setStatus: [status: 'published' | 'draft']; delete: []; runAction: [action: SerializedAction]; clear: [] }>()
11
-
12
- const { t, lang } = useT()
13
- const actionLabel = (action: SerializedAction) => resolveLocalized(action.label, lang.value) ?? action.name
14
- </script>
15
-
16
- <template>
17
- <div class="list__bulkbar">
18
- <span class="list__bulk-count">{{ t('list.selected', { n: count }) }}</span>
19
- <div class="list__bulk-actions">
20
- <template v-if="hasStatus">
21
- <KestrelUiButton type="button" size="sm" variant="secondary" :disabled="busy" @click="emit('setStatus', 'published')">{{ t('list.bulkPublish') }}</KestrelUiButton>
22
- <KestrelUiButton type="button" size="sm" variant="secondary" :disabled="busy" @click="emit('setStatus', 'draft')">{{ t('list.bulkUnpublish') }}</KestrelUiButton>
23
- </template>
24
- <KestrelUiButton
25
- v-for="action in props.actions"
26
- :key="action.name"
27
- type="button"
28
- size="sm"
29
- variant="secondary"
30
- :disabled="busy"
31
- :data-action="action.name"
32
- @click="emit('runAction', action)"
33
- >{{ actionLabel(action) }}</KestrelUiButton>
34
- <KestrelUiButton type="button" size="sm" variant="danger" :disabled="busy" @click="emit('delete')">{{ t('list.bulkDelete') }}</KestrelUiButton>
35
- <KestrelUiButton type="button" size="sm" variant="ghost" @click="emit('clear')">{{ t('list.clearSelection') }}</KestrelUiButton>
36
- </div>
37
- </div>
38
- </template>
39
-
40
- <style lang="scss">
41
- .list {
42
- &__bulkbar {
43
- display: flex;
44
- align-items: center;
45
- gap: var(--space-3);
46
- padding: var(--space-2) var(--space-3);
47
- border: 1px solid var(--color-border);
48
- border-radius: var(--radius-md);
49
- background: var(--color-surface);
50
- }
51
- &__bulk-count {
52
- font-size: var(--text-sm);
53
- font-weight: var(--weight-medium);
54
- }
55
- &__bulk-actions {
56
- display: flex;
57
- align-items: center;
58
- gap: var(--space-2);
59
- margin-inline-start: auto;
60
- }
61
- }
62
- </style>
@@ -1,37 +0,0 @@
1
- <script setup lang="ts">
2
- import { columnLabel } from '../utils/list-cell'
3
- import type { ListColumn } from '../utils/list-columns'
4
-
5
- defineProps<{ columns: ListColumn[]; visibleKeys: string[]; isDefault: boolean }>()
6
- const emit = defineEmits<{ toggle: [key: string]; reset: [] }>()
7
-
8
- const { t } = useT()
9
- const colLabel = (col: ListColumn) => columnLabel(col, t)
10
- </script>
11
-
12
- <template>
13
- <div id="list-columns-panel" class="list__panel">
14
- <!-- eslint-disable-next-line vuejs-accessibility/label-has-for -- native wrapping label around a custom UiCheckbox; no `for`/`id` pair needed, invisible to static analysis -->
15
- <label v-for="c in columns" :key="c.key" class="list__col-row">
16
- <KestrelUiCheckbox
17
- class="list__col-check"
18
- :data-col="c.key"
19
- :model-value="visibleKeys.includes(c.key)"
20
- :disabled="visibleKeys.length === 1 && visibleKeys.includes(c.key)"
21
- @update:model-value="() => emit('toggle', c.key)"
22
- />
23
- <span>{{ colLabel(c) }}</span>
24
- </label>
25
- <button v-if="!isDefault" type="button" class="list__panel-action" @click="emit('reset')">{{ t('list.columnsReset') }}</button>
26
- </div>
27
- </template>
28
-
29
- <style lang="scss">
30
- .list__col-row {
31
- display: flex;
32
- align-items: center;
33
- gap: var(--space-2);
34
- font-size: var(--text-sm);
35
- cursor: pointer;
36
- }
37
- </style>
@@ -1,145 +0,0 @@
1
- <script setup lang="ts">
2
- import type { FilterOp } from '@michaelthielemann/kestrel-core'
3
- import type { ListFilterDraft } from '../composables/useListFilterDraft'
4
- import { columnLabel } from '../utils/list-cell'
5
- import type { ListColumn } from '../utils/list-columns'
6
-
7
- // The whole draft controller arrives as ONE prop: its ten members are a single cohesive thing, and the
8
- // panel must share the parent's draft state (the chip row renders off the same clauses).
9
- const props = defineProps<{ columns: ListColumn[]; filter: ListFilterDraft; hasActive: boolean }>()
10
-
11
- const { t } = useT()
12
- const colLabel = (col: ListColumn) => columnLabel(col, t)
13
- const draft = computed(() => props.filter.draft)
14
- </script>
15
-
16
- <template>
17
- <div id="list-filter-panel" class="list__panel list__panel--filter">
18
- <p v-if="!columns.length" class="list__panel-empty">{{ t('list.noFilters') }}</p>
19
- <div v-for="c in columns" :key="c.key" class="list__filter-row">
20
- <span class="list__filter-label">{{ colLabel(c) }}</span>
21
- <div class="list__filter-controls">
22
- <select
23
- v-if="filter.opsFor(c).length > 1"
24
- class="list__filter-op"
25
- :data-filter-op="c.key"
26
- :aria-label="`${t('filter.operator')} — ${colLabel(c)}`"
27
- :value="draft[c.key]?.op"
28
- @change="filter.setOp(c.key, ($event.target as HTMLSelectElement).value as FilterOp)"
29
- >
30
- <option v-for="op in filter.opsFor(c)" :key="op" :value="op">{{ filter.opLabel(c.filterKind ?? 'text', op) }}</option>
31
- </select>
32
-
33
- <input
34
- v-if="c.filterKind === 'datetime'"
35
- type="date"
36
- class="list__filter"
37
- :data-filter="c.key"
38
- :aria-label="colLabel(c)"
39
- :value="draft[c.key]?.value ?? ''"
40
- @input="filter.setValue(c.key, ($event.target as HTMLInputElement).value)"
41
- >
42
- <input
43
- v-else-if="c.filterKind === 'number' || c.filterKind === 'ref' || c.filterKind === 'idSet'"
44
- type="number"
45
- class="list__filter"
46
- :data-filter="c.key"
47
- :aria-label="colLabel(c)"
48
- :value="draft[c.key]?.value ?? ''"
49
- @input="filter.setValue(c.key, ($event.target as HTMLInputElement).value)"
50
- >
51
- <select
52
- v-else-if="c.filterKind === 'boolean'"
53
- class="list__filter"
54
- :data-filter="c.key"
55
- :aria-label="colLabel(c)"
56
- :value="draft[c.key]?.value ?? ''"
57
- @change="filter.setValue(c.key, ($event.target as HTMLSelectElement).value)"
58
- >
59
- <option value="">—</option>
60
- <option value="true">{{ t('filter.bool.true') }}</option>
61
- <option value="false">{{ t('filter.bool.false') }}</option>
62
- </select>
63
- <!-- Single choice (`enum`) and multi choice (`stringSet`) both pick a stored VALUE from the
64
- field's choices; a free-text box would let the user type the visible LABEL and match nothing. -->
65
- <select
66
- v-else-if="c.filterKind === 'enum' || c.filterKind === 'stringSet'"
67
- class="list__filter"
68
- :data-filter="c.key"
69
- :aria-label="colLabel(c)"
70
- :value="draft[c.key]?.value ?? ''"
71
- @change="filter.setValue(c.key, ($event.target as HTMLSelectElement).value)"
72
- >
73
- <option value="">—</option>
74
- <option v-for="o in filter.enumOptions(c)" :key="o.value" :value="o.value">{{ o.label }}</option>
75
- </select>
76
- <input
77
- v-else
78
- type="text"
79
- class="list__filter"
80
- :data-filter="c.key"
81
- :aria-label="colLabel(c)"
82
- :value="draft[c.key]?.value ?? ''"
83
- :placeholder="t('list.filterPlaceholder', { name: colLabel(c) })"
84
- @input="filter.setValue(c.key, ($event.target as HTMLInputElement).value)"
85
- >
86
- </div>
87
- </div>
88
- <button v-if="hasActive" type="button" class="list__panel-action" @click="filter.clearAll">{{ t('list.filterClearAll') }}</button>
89
- </div>
90
- </template>
91
-
92
- <style lang="scss">
93
- .list {
94
- // The inline filter rows need room for name + operator + value; the columns panel stays compact.
95
- &__panel--filter {
96
- min-width: 30rem;
97
- }
98
- // Name, operator and value on ONE line: a stacked label doubled the panel's height for no information.
99
- &__filter-row {
100
- display: flex;
101
- align-items: center;
102
- gap: var(--space-2);
103
- font-size: var(--text-sm);
104
- }
105
- // Fixed basis so the names form an aligned column and the controls all start at the same x.
106
- &__filter-label {
107
- flex: 0 0 6.5rem;
108
- min-width: 0;
109
- overflow: hidden;
110
- text-overflow: ellipsis;
111
- white-space: nowrap;
112
- color: var(--color-text-muted);
113
- }
114
- // Operator select + value control; the value control takes the remaining width.
115
- &__filter-controls {
116
- flex: 1 1 auto;
117
- min-width: 0;
118
- display: flex;
119
- gap: var(--space-1);
120
- align-items: center;
121
- }
122
- &__filter-op {
123
- flex: 0 0 auto;
124
- max-width: 45%;
125
- padding: var(--space-1) var(--space-2);
126
- border: 1px solid var(--color-border);
127
- border-radius: var(--radius-sm);
128
- background: var(--color-surface);
129
- color: var(--color-text-muted);
130
- font: inherit;
131
- font-size: var(--text-sm);
132
- }
133
- &__filter {
134
- width: 100%;
135
- padding: var(--space-1) var(--space-2);
136
- border: 1px solid var(--color-border);
137
- border-radius: var(--radius-sm);
138
- font: inherit;
139
- }
140
- &__filter-controls .list__filter {
141
- flex: 1 1 auto;
142
- min-width: 0;
143
- }
144
- }
145
- </style>