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