@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,460 +0,0 @@
1
- <script setup lang="ts">
2
- import { computed, ref, nextTick } from 'vue'
3
- import UiIcon from '../ui/Icon.vue'
4
- import FieldLayout from './Layout.vue'
5
- import { useRepeater } from '../../composables/useRepeater'
6
- import { useFieldA11y } from '../../utils/useFieldA11y'
7
- import type { FieldComponentProps } from '../../utils/field-component'
8
- import type { FieldDef, FieldOf, LayoutNode } from '@michaelthielemann/kestrel-core'
9
- const props = defineProps<FieldComponentProps>()
10
- const model = defineModel<Record<string, unknown>[] | null>()
11
-
12
- const subFields = computed<Record<string, FieldDef>>(() =>
13
- props.field.type === 'repeater' ? (props.field as FieldOf<'repeater'>).options.fields : {})
14
- // The repeater's optional sub-field layout (carried on the serialized options; undefined → flat). One
15
- // documented cast, same category as the repeater arm's own narrowing above.
16
- const subLayout = computed<LayoutNode[] | undefined>(() =>
17
- (props.field as { options?: { fieldLayout?: LayoutNode[] } }).options?.fieldLayout)
18
- const required = computed(() => !!props.field.required)
19
- const { rows, keys, addRow, removeRow, move, setCell, insertRow, duplicateRow } = useRepeater(model, subFields)
20
- const { errId, describedby, ariaInvalid } = useFieldA11y(props)
21
- const { t } = useT()
22
-
23
- const rowsEl = ref<HTMLElement | null>(null)
24
- const addEl = ref<HTMLButtonElement | null>(null)
25
- const dragIndex = ref<number | null>(null)
26
- const overIndex = ref<number | null>(null)
27
- const liveMessage = ref('')
28
-
29
- // Toggle a zero-width suffix so an identical message still re-triggers the polite live region.
30
- function say(msg: string) {
31
- liveMessage.value = msg === liveMessage.value ? `${msg}\u200B` : msg
32
- }
33
- function announce(to: number) {
34
- say(t('field.repeater.moved', { n: to + 1, total: rows.value.length }))
35
- }
36
-
37
- function onDragStart(i: number, event: DragEvent) {
38
- if (props.disabled) return
39
- dragIndex.value = i
40
- if (event.dataTransfer) {
41
- event.dataTransfer.effectAllowed = 'move'
42
- event.dataTransfer.setData('text/plain', String(i))
43
- }
44
- }
45
-
46
- function onDragEnter(i: number) {
47
- if (dragIndex.value !== null) overIndex.value = i
48
- }
49
-
50
- // Clear drop highlight only when leaving the list entirely (not between sibling rows).
51
- function onDragLeave(event: DragEvent) {
52
- if (!(event.currentTarget as Element).contains(event.relatedTarget as Node | null)) {
53
- overIndex.value = null
54
- }
55
- }
56
-
57
- function onDrop(i: number) {
58
- if (props.disabled || dragIndex.value === null) return
59
- if (dragIndex.value !== i) {
60
- move(dragIndex.value, i)
61
- nextTick(() => announce(i))
62
- }
63
- dragIndex.value = null
64
- overIndex.value = null
65
- }
66
-
67
- function onDragEnd() {
68
- dragIndex.value = null
69
- overIndex.value = null
70
- }
71
-
72
- function moveRow(from: number, to: number) {
73
- move(from, to)
74
- nextTick(() => {
75
- announce(to)
76
- const row = rowsEl.value?.querySelectorAll(':scope > .ui-repeater__row-wrap > .ui-repeater__row')[to] as HTMLElement | undefined
77
- if (!row) return
78
- const dir = to < from ? 'up' : 'down'
79
- const btns = row.querySelectorAll<HTMLButtonElement>(':scope > .ui-repeater__actions > .ui-repeater__move')
80
- const dirBtn = dir === 'up' ? btns[0] : btns[1]
81
- ;(dirBtn && !dirBtn.disabled ? dirBtn : row.querySelector<HTMLButtonElement>(':scope > .ui-repeater__actions > .ui-repeater__remove'))?.focus()
82
- })
83
- }
84
-
85
- // Keep keyboard focus in the list after deletion: the row that slid into this index, else the new last row, else Add.
86
- function removeRowAt(i: number) {
87
- removeRow(i)
88
- say(t('field.repeater.removed', { n: i + 1, remaining: rows.value.length }))
89
- nextTick(() => {
90
- const idx = Math.min(i, rows.value.length - 1)
91
- const row = idx >= 0
92
- ? rowsEl.value?.querySelectorAll(':scope > .ui-repeater__row-wrap > .ui-repeater__row')[idx] as HTMLElement | undefined
93
- : undefined
94
- ;(row?.querySelector<HTMLButtonElement>(':scope > .ui-repeater__actions > .ui-repeater__remove') ?? addEl.value)?.focus()
95
- })
96
- }
97
-
98
- function duplicateRowAt(i: number) {
99
- duplicateRow(i)
100
- say(t('field.repeater.duplicated', { n: i + 1 }))
101
- nextTick(() => {
102
- const row = rowsEl.value?.querySelectorAll(':scope > .ui-repeater__row-wrap > .ui-repeater__row')[i + 1] as HTMLElement | undefined
103
- row?.querySelector<HTMLElement>('input, button:not([disabled]), [tabindex]:not([tabindex="-1"])')?.focus()
104
- })
105
- }
106
-
107
- function insertRowAt(at: number) {
108
- insertRow(at)
109
- say(t('field.repeater.inserted', { n: at + 1 }))
110
- nextTick(() => {
111
- const row = rowsEl.value?.querySelectorAll(':scope > .ui-repeater__row-wrap > .ui-repeater__row')[at] as HTMLElement | undefined
112
- row?.querySelector<HTMLElement>('input, button:not([disabled]), [tabindex]:not([tabindex="-1"])')?.focus()
113
- })
114
- }
115
- </script>
116
-
117
- <template>
118
- <fieldset
119
- class="ui-repeater"
120
- :aria-describedby="describedby"
121
- :aria-invalid="ariaInvalid"
122
- >
123
- <legend class="ui-repeater__legend">
124
- {{ name }}<span v-if="required" aria-hidden="true">*</span>
125
- </legend>
126
-
127
- <!-- eslint-disable-next-line vuejs-accessibility/no-static-element-interactions -- @dragleave is a mouse-only progressive enhancement; the move-up/move-down buttons below give the same reorder fully keyboard access -->
128
- <div ref="rowsEl" class="ui-repeater__rows" @dragleave="onDragLeave">
129
- <template v-for="(row, i) in rows" :key="keys[i]">
130
- <div class="ui-repeater__insert-zone">
131
- <button
132
- type="button"
133
- class="ui-repeater__insert"
134
- :aria-label="t('field.repeater.insert_label', { n: i + 1 })"
135
- :disabled="disabled"
136
- @click="insertRowAt(i)"
137
- >
138
- <UiIcon name="plus" :size="16" />
139
- </button>
140
- </div>
141
-
142
- <!-- eslint-disable-next-line vuejs-accessibility/no-static-element-interactions -- drag handlers are a mouse-only progressive enhancement; the move-up/move-down buttons below give the same reorder fully keyboard access -->
143
- <div
144
- class="ui-repeater__row-wrap"
145
- :class="{ 'ui-repeater__row-wrap--over': overIndex === i && dragIndex !== i }"
146
- @dragenter="onDragEnter(i)"
147
- @dragover.prevent="onDragEnter(i)"
148
- @drop.prevent="onDrop(i)"
149
- @dragend="onDragEnd"
150
- >
151
- <div class="ui-repeater__row" role="group" :aria-label="t('field.repeater.item_label', { n: i + 1 })">
152
- <div
153
- class="ui-repeater__gutter"
154
- :draggable="!disabled"
155
- aria-hidden="true"
156
- @dragstart="onDragStart(i, $event)"
157
- >
158
- <span class="ui-repeater__index">{{ i + 1 }}</span>
159
- <UiIcon name="grip" :size="16" />
160
- </div>
161
-
162
- <div class="ui-repeater__fields">
163
- <!-- The shared field renderer, scoped to this row (the sibling scope for the condition gate).
164
- An unmet condition unmounts the sub-field (not just hides it), so no constraint lingers. -->
165
- <FieldLayout
166
- :layout="subLayout"
167
- :fields="subFields"
168
- :values="row"
169
- :locale="locale"
170
- :disabled="disabled"
171
- @update="(key, value) => setCell(i, key, value)"
172
- />
173
- </div>
174
-
175
- <div class="ui-repeater__actions">
176
- <button
177
- type="button"
178
- class="ui-repeater__move"
179
- :aria-label="t('field.repeater.move_up', { n: i + 1 })"
180
- :disabled="disabled || i === 0"
181
- @click="moveRow(i, i - 1)"
182
- >
183
- <UiIcon name="chevron-up" :size="16" />
184
- </button>
185
- <button
186
- type="button"
187
- class="ui-repeater__move"
188
- :aria-label="t('field.repeater.move_down', { n: i + 1 })"
189
- :disabled="disabled || i === rows.length - 1"
190
- @click="moveRow(i, i + 1)"
191
- >
192
- <UiIcon name="chevron-down" :size="16" />
193
- </button>
194
- <button
195
- type="button"
196
- class="ui-repeater__duplicate"
197
- :aria-label="t('field.repeater.duplicate_label', { n: i + 1 })"
198
- :disabled="disabled"
199
- @click="duplicateRowAt(i)"
200
- >
201
- <UiIcon name="copy" :size="16" />
202
- </button>
203
- <button
204
- type="button"
205
- class="ui-repeater__remove"
206
- :aria-label="t('field.repeater.remove_label', { n: i + 1 })"
207
- :disabled="disabled"
208
- @click="removeRowAt(i)"
209
- >
210
- <UiIcon name="trash" :size="16" />
211
- </button>
212
- </div>
213
- </div>
214
- </div>
215
- </template>
216
-
217
- <div class="ui-repeater__insert-zone">
218
- <button
219
- type="button"
220
- class="ui-repeater__insert"
221
- :aria-label="t('field.repeater.insert_label', { n: rows.length + 1 })"
222
- :disabled="disabled"
223
- @click="insertRowAt(rows.length)"
224
- >
225
- <UiIcon name="plus" :size="16" />
226
- </button>
227
- </div>
228
- </div>
229
-
230
- <p v-if="!rows.length" class="ui-repeater__empty">{{ t('field.repeater.empty') }}</p>
231
-
232
- <button ref="addEl" type="button" class="ui-repeater__add" :disabled="disabled" @click="addRow">{{ t('field.repeater.add') }}</button>
233
-
234
- <p v-if="error" :id="errId" class="ui-repeater__error" role="alert">{{ error }}</p>
235
- <span class="ui-repeater__live" aria-live="polite">{{ liveMessage }}</span>
236
- </fieldset>
237
- </template>
238
-
239
- <style lang="scss">
240
- @use '../../assets/scss/mixins';
241
-
242
- .ui-repeater {
243
- display: flex;
244
- flex-direction: column;
245
- gap: var(--space-3);
246
- min-inline-size: 0;
247
- margin: 0;
248
- padding: 0;
249
- border: 0;
250
-
251
- &__legend {
252
- padding: 0;
253
- font-size: var(--text-sm);
254
- font-weight: var(--weight-medium);
255
- margin-bottom: var(--space-1);
256
- }
257
-
258
- &__rows {
259
- display: flex;
260
- flex-direction: column;
261
- }
262
-
263
- &__insert-zone {
264
- position: relative;
265
- display: flex;
266
- align-items: center;
267
- justify-content: center;
268
- height: 1.5rem;
269
-
270
- &::before {
271
- content: '';
272
- position: absolute;
273
- left: 0;
274
- right: 0;
275
- top: 50%;
276
- transform: translateY(-50%);
277
- height: 1px;
278
- background: var(--color-border);
279
- opacity: 0;
280
- transition: opacity 0.1s;
281
- }
282
-
283
- &:hover {
284
- &::before { opacity: 1; }
285
- .ui-repeater__insert { opacity: 1; }
286
- }
287
- // Instant on keyboard focus so the focus ring is not hidden by the opacity transition.
288
- &:focus-within {
289
- &::before { opacity: 1; }
290
- .ui-repeater__insert { opacity: 1; transition: none; }
291
- }
292
- }
293
-
294
- &__insert {
295
- @include mixins.focus-ring;
296
- position: relative;
297
- z-index: 1;
298
- display: flex;
299
- align-items: center;
300
- justify-content: center;
301
- width: 1.75rem;
302
- height: 1.75rem;
303
- border: 1px solid var(--color-border);
304
- border-radius: var(--radius-sm);
305
- background: var(--color-surface);
306
- cursor: pointer;
307
- color: var(--color-text-muted);
308
- opacity: 0;
309
- transition: opacity 0.1s;
310
-
311
- &:hover:not(:disabled) {
312
- background: var(--color-bg);
313
- color: var(--color-text);
314
- }
315
-
316
- &:disabled { cursor: default; }
317
- }
318
-
319
- @media (hover: none) {
320
- &__insert-zone::before { opacity: 1; }
321
- &__insert { opacity: 1; }
322
- }
323
-
324
- &__row-wrap {
325
- &--over {
326
- .ui-repeater__row {
327
- outline: 2px solid var(--color-primary);
328
- outline-offset: -2px;
329
- }
330
- }
331
- }
332
-
333
- &__row {
334
- display: grid;
335
- grid-template-columns: auto minmax(0, 1fr) auto;
336
- gap: var(--space-2);
337
- align-items: start;
338
- padding: var(--space-3);
339
- border: 1px solid var(--color-border);
340
- border-radius: var(--radius-md);
341
- background: var(--color-surface);
342
-
343
- // Keyboard focus must reveal the actions instantly: an opacity transition would hide the
344
- // focus ring for its duration (the buttons sit in an opacity:0 layer). Hover stays animated.
345
- &:hover .ui-repeater__actions { opacity: 1; }
346
- &:focus-within .ui-repeater__actions { opacity: 1; transition: none; }
347
- }
348
-
349
- &__gutter {
350
- display: flex;
351
- flex-direction: column;
352
- align-items: center;
353
- gap: var(--space-1);
354
- padding-top: var(--space-1);
355
- cursor: grab;
356
- color: var(--color-text-muted);
357
- user-select: none;
358
-
359
- &:active { cursor: grabbing; }
360
- }
361
-
362
- &__index {
363
- font-size: var(--text-sm);
364
- line-height: 1;
365
- }
366
-
367
- &__fields {
368
- display: flex;
369
- flex-direction: column;
370
- gap: var(--space-2);
371
- min-inline-size: 0;
372
- }
373
-
374
- &__actions {
375
- display: flex;
376
- flex-direction: column;
377
- gap: var(--space-1);
378
- opacity: 0;
379
- transition: opacity 0.1s;
380
- }
381
-
382
- @media (hover: none) {
383
- &__actions { opacity: 1; }
384
- }
385
-
386
- &__move,
387
- &__duplicate,
388
- &__remove {
389
- @include mixins.focus-ring;
390
- display: flex;
391
- align-items: center;
392
- justify-content: center;
393
- width: 1.75rem;
394
- height: 1.75rem;
395
- border: 1px solid transparent;
396
- border-radius: var(--radius-sm);
397
- background: transparent;
398
- cursor: pointer;
399
- color: var(--color-text-muted);
400
- transition: background 0.1s, border-color 0.1s, color 0.1s;
401
-
402
- &:hover:not(:disabled) {
403
- background: var(--color-bg);
404
- border-color: var(--color-border);
405
- color: var(--color-text);
406
- }
407
-
408
- &:disabled {
409
- opacity: 0.4;
410
- cursor: default;
411
- }
412
- }
413
-
414
- &__remove {
415
- color: var(--color-danger);
416
-
417
- &:hover:not(:disabled) {
418
- color: var(--color-danger);
419
- border-color: var(--color-danger);
420
- }
421
- }
422
-
423
- &__empty {
424
- font-size: var(--text-sm);
425
- color: var(--color-text-muted);
426
- }
427
-
428
- &__add {
429
- @include mixins.focus-ring;
430
- align-self: flex-start;
431
- border: 1px solid var(--color-border);
432
- background: var(--color-surface);
433
- border-radius: var(--radius-sm);
434
- padding: var(--space-1) var(--space-3);
435
- cursor: pointer;
436
- font-size: var(--text-sm);
437
- color: var(--color-text);
438
-
439
- &:disabled {
440
- opacity: 0.4;
441
- cursor: default;
442
- }
443
- }
444
-
445
- &__error {
446
- font-size: var(--text-sm);
447
- color: var(--color-danger);
448
- }
449
-
450
- &__live {
451
- @include mixins.sr-only;
452
- }
453
-
454
- @media (prefers-reduced-motion: reduce) {
455
- &__actions,
456
- &__insert,
457
- &__insert-zone::before { transition: none; }
458
- }
459
- }
460
- </style>
@@ -1,26 +0,0 @@
1
- <script setup lang="ts">
2
- import { computed } from 'vue'
3
- import UiField from '../ui/Field.vue'
4
- import UiRichtext from '../ui/Richtext.vue'
5
- import type { FieldComponentProps } from '../../utils/field-component'
6
-
7
- const props = defineProps<FieldComponentProps>()
8
- const model = defineModel<string | null>()
9
- const required = computed(() => !!props.field.required)
10
- </script>
11
-
12
- <template>
13
- <UiField :id="id" :label="name" :error="error" :required="required">
14
- <template #default="f">
15
- <UiRichtext
16
- v-model="model"
17
- :disabled="disabled"
18
- :locale="locale"
19
- :aria-label="name"
20
- :describedby="f['aria-describedby']"
21
- :invalid="f['aria-invalid'] === 'true'"
22
- :required="!!f.required"
23
- />
24
- </template>
25
- </UiField>
26
- </template>
@@ -1,65 +0,0 @@
1
- <script setup lang="ts">
2
- import { computed } from 'vue'
3
- import { slugify } from '@michaelthielemann/kestrel-core/client'
4
- import type { FieldOf } from '@michaelthielemann/kestrel-core'
5
- import type { FieldComponentProps } from '../../utils/field-component'
6
- // Editor widget for the `slug` field type. Shows the (display-only) `prefix` before an editable slug input,
7
- // and normalizes the value to a real slug on blur. Leaving it blank is fine — the server auto-generates it
8
- // from `options.from` (e.g. the title) on save. `options.from`/`prefix` come from the field def.
9
- const props = defineProps<FieldComponentProps>()
10
- const model = defineModel<string | null>()
11
-
12
- const opts = computed(() => (props.field.type === 'slug' ? (props.field as FieldOf<'slug'>).options : undefined))
13
- const prefix = computed(() => opts.value?.prefix ?? '')
14
- const fromField = computed(() => opts.value?.from)
15
-
16
- // Blur must not turn a null (never-touched, server-auto-generates) slug into '' — that would mark
17
- // an untouched record dirty. Only normalize when there is actual text, and only write if it changes.
18
- function onBlur() {
19
- const cur = model.value ?? ''
20
- if (!cur.trim()) return
21
- const s = slugify(cur)
22
- if (s && s !== cur) model.value = s
23
- }
24
- </script>
25
-
26
- <template>
27
- <KestrelUiField :id="id" :label="name" :error="error ?? undefined">
28
- <template #default="f">
29
- <div class="field-slug" :data-prefixed="!!prefix">
30
- <span v-if="prefix" class="field-slug__prefix">{{ prefix }}</span>
31
- <input
32
- :id="f.id"
33
- class="field-slug__input"
34
- type="text"
35
- :value="model ?? ''"
36
- :placeholder="fromField ? `auto-generated from ${fromField}` : 'slug'"
37
- :aria-invalid="f['aria-invalid']"
38
- :aria-describedby="f['aria-describedby']"
39
- :disabled="disabled"
40
- @input="model = ($event.target as HTMLInputElement).value"
41
- @blur="onBlur"
42
- />
43
- </div>
44
- </template>
45
- </KestrelUiField>
46
- </template>
47
-
48
- <style lang="scss" scoped>
49
- .field-slug {
50
- display: flex; align-items: stretch; min-width: 0;
51
- border: 1px solid var(--color-control-border, var(--color-border)); border-radius: var(--radius-md);
52
- background: var(--color-surface); overflow: hidden;
53
- }
54
- .field-slug__prefix {
55
- display: inline-flex; align-items: center; padding: var(--space-2) var(--space-3);
56
- background: var(--color-surface-2); color: var(--color-text-muted);
57
- border-right: 1px solid var(--color-border); font-size: var(--text-sm); white-space: nowrap;
58
- }
59
- .field-slug__input {
60
- flex: 1 1 auto; min-width: 0; padding: var(--space-2) var(--space-3);
61
- border: 0; background: transparent; color: var(--color-text); font: inherit;
62
- &:focus { outline: none; }
63
- }
64
- .field-slug:focus-within { border-color: var(--color-primary, #6366f1); }
65
- </style>
@@ -1,35 +0,0 @@
1
- <script setup lang="ts">
2
- import { computed } from 'vue'
3
- import UiField from '../ui/Field.vue'
4
- import UiTextInput from '../ui/TextInput.vue'
5
- import UiTextarea from '../ui/Textarea.vue'
6
- import { fieldConstraints } from '@michaelthielemann/kestrel-fields/client'
7
- import type { FieldComponentProps } from '../../utils/field-component'
8
-
9
- const props = defineProps<FieldComponentProps>()
10
- const model = defineModel<string | null>()
11
- const c = computed(() => fieldConstraints(props.field))
12
- </script>
13
-
14
- <template>
15
- <UiField :id="id" :label="name" :error="error" :required="c.required">
16
- <template #default="f">
17
- <UiTextarea
18
- v-if="c.multiline"
19
- v-model="model"
20
- :disabled="disabled"
21
- :maxlength="c.maxlength"
22
- :minlength="c.minlength"
23
- v-bind="f"
24
- />
25
- <UiTextInput
26
- v-else
27
- v-model="model"
28
- :disabled="disabled"
29
- :maxlength="c.maxlength"
30
- :minlength="c.minlength"
31
- v-bind="f"
32
- />
33
- </template>
34
- </UiField>
35
- </template>
@@ -1,34 +0,0 @@
1
- <script setup lang="ts">
2
- import type { FieldComponentProps } from '../../utils/field-component'
3
-
4
- defineProps<FieldComponentProps>()
5
- defineModel<unknown>()
6
- </script>
7
-
8
- <template>
9
- <div class="field-unsupported">
10
- <span class="field-unsupported__label">{{ name }}</span>
11
- <p class="field-unsupported__note">Field type "{{ field.type }}" is not yet available.</p>
12
- </div>
13
- </template>
14
-
15
- <style lang="scss">
16
- .field-unsupported {
17
- display: flex;
18
- flex-direction: column;
19
- gap: var(--space-1);
20
-
21
- &__label {
22
- font-size: var(--text-sm);
23
- font-weight: var(--weight-medium);
24
- }
25
- &__note {
26
- font-size: var(--text-sm);
27
- color: var(--color-text-muted);
28
- padding: var(--space-2) var(--space-3);
29
- border: 1px solid var(--color-border);
30
- border-radius: var(--radius-md);
31
- background: var(--color-surface-2);
32
- }
33
- }
34
- </style>
@@ -1,50 +0,0 @@
1
- <script setup lang="ts">
2
- import { computed } from 'vue'
3
-
4
- const props = withDefaults(
5
- defineProps<{
6
- variant?: 'info' | 'success' | 'warning' | 'error'
7
- }>(),
8
- { variant: 'info' },
9
- )
10
-
11
- const role = computed(() =>
12
- props.variant === 'error' || props.variant === 'warning' ? 'alert' : 'status',
13
- )
14
- </script>
15
-
16
- <template>
17
- <div class="ui-alert" :class="`ui-alert--${variant}`" :role="role">
18
- <p v-if="$slots.title?.()?.[0]" class="ui-alert__title"><slot name="title" /></p>
19
- <div class="ui-alert__body"><slot /></div>
20
- </div>
21
- </template>
22
-
23
- <style lang="scss">
24
- .ui-alert {
25
- padding: var(--space-3) var(--space-4);
26
- border: 1px solid var(--color-border);
27
- border-inline-start-width: 4px;
28
- border-radius: var(--radius-md);
29
- background: var(--color-surface);
30
- font-size: var(--text-sm);
31
-
32
- &__title {
33
- font-weight: var(--weight-bold);
34
- margin-bottom: var(--space-1);
35
- }
36
-
37
- &--info {
38
- border-inline-start-color: var(--color-primary);
39
- }
40
- &--success {
41
- border-inline-start-color: var(--color-success);
42
- }
43
- &--warning {
44
- border-inline-start-color: var(--color-warning);
45
- }
46
- &--error {
47
- border-inline-start-color: var(--color-danger);
48
- }
49
- }
50
- </style>
@@ -1,22 +0,0 @@
1
- <script setup lang="ts">
2
- // The Kestrel brand mark. The wordmark and the surrounding wrapper (a NuxtLink in the rail, a static
3
- // heading on the login screen) stay with each caller.
4
- </script>
5
-
6
- <template>
7
- <span class="ui-brand-mark" aria-hidden="true">K</span>
8
- </template>
9
-
10
- <style lang="scss">
11
- .ui-brand-mark {
12
- display: inline-flex;
13
- align-items: center;
14
- justify-content: center;
15
- flex-shrink: 0;
16
- width: 1.75rem;
17
- height: 1.75rem;
18
- border-radius: var(--radius-md);
19
- background: var(--color-primary);
20
- color: var(--color-on-primary);
21
- }
22
- </style>