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