@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,175 +0,0 @@
1
- <script setup lang="ts">
2
- import { computed } from 'vue'
3
-
4
- // Attrs (id, aria-*) must land on the inner <input>, not the suffix wrapper, so the field label's
5
- // `for` keeps pointing at the control. With no suffix the input still receives them via v-bind="$attrs".
6
- defineOptions({ inheritAttrs: false })
7
-
8
- withDefaults(
9
- defineProps<{
10
- min?: number
11
- max?: number
12
- step?: number | 'any'
13
- placeholder?: string
14
- disabled?: boolean
15
- // Display-only suffix (e.g. 'rem'). When set, the input gains a trailing adornment; the emitted
16
- // model value is still a bare number. When absent, the plain `<input class="ui-number">` renders.
17
- suffix?: string
18
- /** Tighter padding for dense chrome (toolbars). Still meets the AA minimum target size. */
19
- slim?: boolean
20
- }>(),
21
- { disabled: false, slim: false },
22
- )
23
-
24
- const model = defineModel<number | null>()
25
-
26
- // Bound with `v-model` rather than :value/@input on purpose: mid-edit text like '1.0' parses to the
27
- // number 1, and a plain :value binding would write that back over the text, deleting the trailing digit
28
- // and moving the caret ('1.05' would end up as 15). v-model's directive skips the DOM write while the
29
- // field is focused and its text still parses to the same number, so typing stays intact.
30
- // The proxy only maps the empty field to null; v-model already hands over a number for a number input.
31
- const text = computed<number | string>({
32
- get: () => model.value ?? '',
33
- set: (v) => { model.value = v === '' ? null : Number(v) },
34
- })
35
-
36
- // A number input can hold editing text the browser can't parse ('-', '1e'); its DOM value is then ''
37
- // and the model already null, so a save would silently drop what the user still sees in the field.
38
- // Snap the text back to the model so the field never shows a value that wouldn't be saved.
39
- // (Assigning `value` clears the unparsable editing text even though the raw value is already ''.)
40
- function snapBack(el: HTMLInputElement) {
41
- if (el.validity?.badInput) el.value = model.value == null ? '' : String(model.value)
42
- }
43
- function onBlur(e: Event) {
44
- snapBack(e.target as HTMLInputElement)
45
- }
46
- // Enter fires the ancestor form's implicit submission WITHOUT a preceding blur, so the blur guard
47
- // can't run — with the form's native validation off, a null would be saved while unparsable text
48
- // still shows. Suppress that submit and snap back so the mismatch is corrected and visible instead.
49
- function onKeydown(e: KeyboardEvent) {
50
- if (e.key !== 'Enter' || e.isComposing) return
51
- const el = e.target as HTMLInputElement
52
- if (el.validity?.badInput) { e.preventDefault(); snapBack(el) }
53
- }
54
- </script>
55
-
56
- <template>
57
- <div v-if="suffix" class="ui-number-wrap" :class="{ 'ui-number-wrap--slim': slim }" :data-disabled="disabled || undefined">
58
- <!-- eslint-disable-next-line vuejs-accessibility/form-control-has-label -- id/label land via v-bind="$attrs" from the wrapping UiField, invisible to static analysis -->
59
- <input
60
- v-model="text"
61
- type="number"
62
- :min="min"
63
- :max="max"
64
- :step="step"
65
- :placeholder="placeholder"
66
- :disabled="disabled"
67
- class="ui-number-wrap__input"
68
- v-bind="$attrs"
69
- @blur="onBlur"
70
- @keydown="onKeydown"
71
- >
72
- <span class="ui-number-wrap__suffix" aria-hidden="true">{{ suffix }}</span>
73
- </div>
74
- <!-- eslint-disable-next-line vuejs-accessibility/form-control-has-label -- id/label land via v-bind="$attrs" from the wrapping UiField, invisible to static analysis -->
75
- <input
76
- v-else
77
- v-model="text"
78
- type="number"
79
- :min="min"
80
- :max="max"
81
- :step="step"
82
- :placeholder="placeholder"
83
- :disabled="disabled"
84
- class="ui-number"
85
- :class="{ 'ui-number--slim': slim }"
86
- v-bind="$attrs"
87
- @blur="onBlur"
88
- @keydown="onKeydown"
89
- >
90
- </template>
91
-
92
- <style lang="scss">
93
- @use '../../assets/scss/mixins';
94
-
95
- .ui-number {
96
- @include mixins.input-base;
97
- width: 100%;
98
-
99
- &::placeholder {
100
- color: var(--color-text-muted);
101
- }
102
- &--slim {
103
- @include mixins.input-slim;
104
- }
105
- }
106
-
107
- // Suffix variant (opt-in via the `suffix` prop). The border/radius live on the wrapper; the inner input
108
- // is borderless + transparent so it reads as one control, mirroring the slug prefix adornment.
109
- .ui-number-wrap {
110
- display: flex;
111
- align-items: stretch;
112
- width: 100%;
113
- min-width: 0;
114
- border: 1px solid var(--color-control-border, var(--color-border));
115
- border-radius: var(--radius-md);
116
- background: var(--color-surface);
117
- overflow: hidden; // clip the square-cornered suffix background to the wrapper's rounded border
118
-
119
- &:focus-within {
120
- outline: 2px solid var(--color-focus);
121
- outline-offset: -2px;
122
- }
123
- &[data-disabled] {
124
- opacity: 0.6;
125
- cursor: not-allowed;
126
- }
127
- // input-base flags the invalid state on the input's own border; here the border lives on the wrapper
128
- // while the inner input is borderless, so mirror it so a unit field shows the same red invalid border.
129
- &:has(.ui-number-wrap__input[aria-invalid='true']) {
130
- border-color: var(--color-danger);
131
- }
132
-
133
- &__input {
134
- flex: 1 1 auto;
135
- min-width: 0;
136
- padding: var(--space-2) var(--space-3);
137
- border: 0;
138
- background: transparent;
139
- color: var(--color-text);
140
- font-size: var(--text-base);
141
-
142
- &:focus-visible {
143
- outline: none;
144
- }
145
- &::placeholder {
146
- color: var(--color-text-muted);
147
- }
148
- &:disabled {
149
- cursor: not-allowed;
150
- }
151
- }
152
-
153
- &__suffix {
154
- display: inline-flex;
155
- align-items: center;
156
- padding-inline: var(--space-3);
157
- background: var(--color-surface-2);
158
- color: var(--color-text-muted);
159
- border-left: 1px solid var(--color-control-border, var(--color-border));
160
- font-size: var(--text-sm);
161
- white-space: nowrap;
162
- }
163
- }
164
-
165
- // Slim adornment variant: the wrapper keeps its border/radius, only the inner box tightens. Written as a
166
- // separate rule (not nested) so `.ui-number-wrap--slim .ui-number-wrap__input` outranks the base element rule.
167
- .ui-number-wrap--slim {
168
- .ui-number-wrap__input {
169
- @include mixins.input-slim;
170
- }
171
- .ui-number-wrap__suffix {
172
- padding-inline: var(--space-2);
173
- }
174
- }
175
- </style>
@@ -1,141 +0,0 @@
1
- <script setup lang="ts">
2
- import { watch, watchEffect } from 'vue'
3
- import { useEditor, EditorContent } from '@tiptap/vue-3'
4
- import { StarterKit } from '@tiptap/starter-kit'
5
- import { Highlight } from '@tiptap/extension-highlight'
6
- import { Subscript } from '@tiptap/extension-subscript'
7
- import { Superscript } from '@tiptap/extension-superscript'
8
- import { TextAlign } from '@tiptap/extension-text-align'
9
- import RichtextToolbar from './RichtextToolbar.vue'
10
- import { RichtextSpanClass, RichtextBlockClass } from '../../utils/richtext-preserve-class'
11
- import { RICHTEXT_LINK_SCHEME } from '@michaelthielemann/kestrel-core/client'
12
-
13
- const props = withDefaults(
14
- defineProps<{
15
- disabled?: boolean
16
- locale?: string
17
- // a11y: the editing surface is a contenteditable with no native label; the field name + error/state
18
- // are set on the ProseMirror DOM node (editor.view.dom) so AT announces a named multiline textbox.
19
- ariaLabel?: string
20
- describedby?: string
21
- invalid?: boolean
22
- required?: boolean
23
- }>(),
24
- { disabled: false },
25
- )
26
- const model = defineModel<string | null>()
27
-
28
- // TipTap emits `update` for non-content changes too — `setEditable`/`setOptions` emit directly, past the
29
- // transaction pipeline and its docChanged guard — and that echo reaches the edit form as a user edit.
30
- // Only a changed doc is one, so every emit is checked against the doc it last saw.
31
- let lastDoc: unknown = null
32
-
33
- // StarterKit (v3) already bundles Link + Underline; only the rest are added here.
34
- const editor = useEditor({
35
- content: model.value ?? '',
36
- editable: !props.disabled,
37
- // (no `immediatelyRender` — not in tiptap v3's EditorOptions type; the admin editor is SPA-only so the
38
- // browser default applies.)
39
- extensions: [
40
- StarterKit.configure({
41
- heading: { levels: [1, 2, 3, 4, 5, 6] },
42
- // `protocols`: the Link mark validates every href against its own scheme allowlist, which has no
43
- // `kestrel:`. Without this it drops the whole anchor when stored content is loaded and makes the
44
- // toolbar's `setLink` a silent no-op — opening and saving a record would destroy its internal links.
45
- // `target`/`rel` null: the sanitizer owns that policy — it decorates absolute http(s) links and
46
- // strips the attributes from every other kind. The mark's own defaults would stamp them on ANY
47
- // anchor, so a stored internal/relative/mailto link came back changed and read as an unsaved edit.
48
- link: { openOnClick: false, protocols: [RICHTEXT_LINK_SCHEME], HTMLAttributes: { target: null, rel: null } },
49
- }),
50
- Highlight,
51
- Subscript,
52
- Superscript,
53
- TextAlign.configure({ types: ['heading', 'paragraph'] }),
54
- RichtextSpanClass, RichtextBlockClass, // preserve consumer presentational classes on round-trip
55
- ],
56
- onCreate: ({ editor }) => {
57
- lastDoc = editor.state.doc
58
- },
59
- onUpdate: ({ editor }) => {
60
- if (editor.state.doc === lastDoc) return
61
- lastDoc = editor.state.doc
62
- model.value = editor.isEmpty ? '' : editor.getHTML()
63
- },
64
- })
65
-
66
- watch(model, (value) => {
67
- const e = editor.value
68
- if (!e) return
69
- const next = value ?? ''
70
- const current = e.isEmpty ? '' : e.getHTML()
71
- // `emitUpdate: false`: this value came FROM the model, so re-emitting it would reach the edit form as
72
- // a user edit — clearing the redo stack and turning a restored null back into ''.
73
- if (next !== current) {
74
- e.commands.setContent(next, { emitUpdate: false })
75
- lastDoc = e.state.doc
76
- }
77
- })
78
-
79
- // `false` — the second arg suppresses the update event; `disabled` tracks the form's `saving` flag, so
80
- // the post-save toggle would otherwise re-dirty the form that was just rebaselined.
81
- watch(() => props.disabled, (d) => editor.value?.setEditable(!d, false))
82
-
83
- // Apply the accessible name + textbox semantics directly to the ProseMirror surface, reactively (the
84
- // error-driven describedby/invalid arrive after mount). `editor.view.dom` is the contenteditable element.
85
- function setAttr(el: HTMLElement, name: string, value?: string) {
86
- if (value) el.setAttribute(name, value)
87
- else el.removeAttribute(name)
88
- }
89
- watchEffect(() => {
90
- const dom = editor.value?.view?.dom as HTMLElement | undefined
91
- if (!dom) return
92
- dom.setAttribute('role', 'textbox')
93
- dom.setAttribute('aria-multiline', 'true')
94
- setAttr(dom, 'aria-label', props.ariaLabel)
95
- setAttr(dom, 'aria-describedby', props.describedby)
96
- setAttr(dom, 'aria-invalid', props.invalid ? 'true' : undefined)
97
- setAttr(dom, 'aria-required', props.required ? 'true' : undefined)
98
- })
99
- </script>
100
-
101
- <template>
102
- <div class="ui-richtext">
103
- <RichtextToolbar :editor="editor" :locale="locale" :disabled="disabled" />
104
- <EditorContent :editor="editor" class="ui-richtext__content" />
105
- </div>
106
- </template>
107
-
108
- <style lang="scss">
109
- .ui-richtext {
110
- border: 1px solid var(--color-control-border, var(--color-border));
111
- border-radius: var(--radius-md);
112
- background: var(--color-surface);
113
-
114
- &__content {
115
- .tiptap {
116
- padding: var(--space-3);
117
- min-height: 8rem;
118
- outline: none;
119
-
120
- :where(p, ul, ol, blockquote, pre, h1, h2, h3, h4, h5, h6) { margin: 0 0 var(--space-3); }
121
- :where(p, ul, ol, blockquote, pre, h1, h2, h3, h4, h5, h6):last-child { margin-bottom: 0; }
122
- ul, ol { padding-inline-start: var(--space-5); }
123
- blockquote {
124
- border-inline-start: 3px solid var(--color-border);
125
- padding-inline-start: var(--space-3);
126
- color: var(--color-text-muted);
127
- }
128
- pre {
129
- background: var(--color-bg);
130
- padding: var(--space-3);
131
- border-radius: var(--radius-sm);
132
- overflow-x: auto;
133
- }
134
- code { font-family: monospace; }
135
- a { color: var(--color-primary); }
136
- mark { background: var(--color-highlight); color: var(--color-on-highlight); }
137
- hr { border: 0; border-top: 1px solid var(--color-border); margin: var(--space-4) 0; }
138
- }
139
- }
140
- }
141
- </style>
@@ -1,251 +0,0 @@
1
- <script setup lang="ts">
2
- import { ref, computed, watch } from 'vue'
3
- import type { Editor } from '@tiptap/vue-3'
4
- import UiButton from './Button.vue'
5
- import UiIcon from './Icon.vue'
6
- import UiSelect from './Select.vue'
7
- import UiTextInput from './TextInput.vue'
8
- import LinkInternalPicker from '../field/LinkInternalPicker.vue'
9
- import type { IconName } from '../../utils/icons'
10
- import { richtextLinkHref, parseRichtextLinkHref } from '@michaelthielemann/kestrel-core/client'
11
-
12
- const props = defineProps<{ editor: Editor | undefined; locale?: string; disabled?: boolean }>()
13
-
14
- interface Item {
15
- name: string
16
- label: string
17
- icon: IconName
18
- run: (e: Editor) => void
19
- active?: (e: Editor) => boolean
20
- disabled?: (e: Editor) => boolean
21
- }
22
-
23
- const { t } = useT()
24
-
25
- const MARKS = computed((): Item[] => [
26
- { name: 'bold', label: t('richtext.bold'), icon: 'bold', run: (e) => e.chain().focus().toggleBold().run(), active: (e) => e.isActive('bold') },
27
- { name: 'italic', label: t('richtext.italic'), icon: 'italic', run: (e) => e.chain().focus().toggleItalic().run(), active: (e) => e.isActive('italic') },
28
- { name: 'underline', label: t('richtext.underline'), icon: 'underline', run: (e) => e.chain().focus().toggleUnderline().run(), active: (e) => e.isActive('underline') },
29
- { name: 'strike', label: t('richtext.strikethrough'), icon: 'strikethrough', run: (e) => e.chain().focus().toggleStrike().run(), active: (e) => e.isActive('strike') },
30
- { name: 'code', label: t('richtext.inlineCode'), icon: 'code', run: (e) => e.chain().focus().toggleCode().run(), active: (e) => e.isActive('code') },
31
- { name: 'highlight', label: t('richtext.highlight'), icon: 'highlighter', run: (e) => e.chain().focus().toggleHighlight().run(), active: (e) => e.isActive('highlight') },
32
- { name: 'subscript', label: t('richtext.subscript'), icon: 'subscript', run: (e) => e.chain().focus().toggleSubscript().run(), active: (e) => e.isActive('subscript') },
33
- { name: 'superscript', label: t('richtext.superscript'), icon: 'superscript', run: (e) => e.chain().focus().toggleSuperscript().run(), active: (e) => e.isActive('superscript') },
34
- ])
35
- const BLOCKS = computed((): Item[] => [
36
- { name: 'bulletList', label: t('richtext.bulletList'), icon: 'list', run: (e) => e.chain().focus().toggleBulletList().run(), active: (e) => e.isActive('bulletList') },
37
- { name: 'orderedList', label: t('richtext.orderedList'), icon: 'list-ordered', run: (e) => e.chain().focus().toggleOrderedList().run(), active: (e) => e.isActive('orderedList') },
38
- { name: 'blockquote', label: t('richtext.blockquote'), icon: 'quote', run: (e) => e.chain().focus().toggleBlockquote().run(), active: (e) => e.isActive('blockquote') },
39
- { name: 'codeBlock', label: t('richtext.codeBlock'), icon: 'square-code', run: (e) => e.chain().focus().toggleCodeBlock().run(), active: (e) => e.isActive('codeBlock') },
40
- { name: 'horizontalRule', label: t('richtext.horizontalRule'), icon: 'minus', run: (e) => e.chain().focus().setHorizontalRule().run() },
41
- ])
42
- const ALIGN = computed((): Item[] => [
43
- { name: 'left', label: t('richtext.alignLeft'), icon: 'align-left', run: (e) => e.chain().focus().setTextAlign('left').run(), active: (e) => e.isActive({ textAlign: 'left' }) },
44
- { name: 'center', label: t('richtext.alignCenter'), icon: 'align-center', run: (e) => e.chain().focus().setTextAlign('center').run(), active: (e) => e.isActive({ textAlign: 'center' }) },
45
- { name: 'right', label: t('richtext.alignRight'), icon: 'align-right', run: (e) => e.chain().focus().setTextAlign('right').run(), active: (e) => e.isActive({ textAlign: 'right' }) },
46
- { name: 'justify', label: t('richtext.justify'), icon: 'align-justify', run: (e) => e.chain().focus().setTextAlign('justify').run(), active: (e) => e.isActive({ textAlign: 'justify' }) },
47
- ])
48
- const EXTRA = computed((): Item[] => [
49
- { name: 'clear', label: t('richtext.clearFormatting'), icon: 'eraser', run: (e) => e.chain().focus().unsetAllMarks().clearNodes().run() },
50
- { name: 'undo', label: t('richtext.undo'), icon: 'undo', run: (e) => e.chain().focus().undo().run(), disabled: (e) => !e.can().undo() },
51
- { name: 'redo', label: t('richtext.redo'), icon: 'redo', run: (e) => e.chain().focus().redo().run(), disabled: (e) => !e.can().redo() },
52
- ])
53
-
54
- const BLOCK_OPTIONS = computed(() => [
55
- { label: t('richtext.paragraph'), value: 'p' },
56
- { label: t('richtext.heading1'), value: 'h1' }, { label: t('richtext.heading2'), value: 'h2' }, { label: t('richtext.heading3'), value: 'h3' },
57
- { label: t('richtext.heading4'), value: 'h4' }, { label: t('richtext.heading5'), value: 'h5' }, { label: t('richtext.heading6'), value: 'h6' },
58
- ])
59
-
60
- // Re-render the toolbar on every editor transaction so active/disabled states stay live.
61
- const tick = ref(0)
62
- watch(() => props.editor, (editor, _old, onCleanup) => {
63
- if (!editor) return
64
- const bump = () => { tick.value++ }
65
- editor.on('transaction', bump)
66
- onCleanup(() => { editor.off('transaction', bump) })
67
- }, { immediate: true })
68
-
69
- const blockValue = computed(() => {
70
- void tick.value
71
- const e = props.editor
72
- if (!e) return 'p'
73
- for (let l = 1; l <= 6; l++) if (e.isActive('heading', { level: l })) return `h${l}`
74
- return 'p'
75
- })
76
- function onBlock(v: string | null | undefined) {
77
- const e = props.editor
78
- if (!e || !v) return
79
- if (v === 'p') e.chain().focus().setParagraph().run()
80
- else e.chain().focus().toggleHeading({ level: Number(v[1]) as 1 | 2 | 3 | 4 | 5 | 6 }).run()
81
- }
82
-
83
- const linkOpen = ref(false)
84
- const linkUrl = ref('')
85
- function toggleLink() {
86
- const e = props.editor!
87
- if (e.isActive('link')) { e.chain().focus().unsetLink().run(); return }
88
- linkInternalOpen.value = false
89
- linkUrl.value = (e.getAttributes('link').href as string) ?? ''
90
- linkOpen.value = true
91
- }
92
- function applyLink() {
93
- const e = props.editor!
94
- const href = linkUrl.value.trim()
95
- if (href) e.chain().focus().extendMarkRange('link').setLink({ href }).run()
96
- linkOpen.value = false
97
- linkUrl.value = ''
98
- }
99
-
100
- // Internal links: pick a record, store it as a `kestrel:<collection>:<id>` marker on a normal link
101
- // mark (resolved to the real localized path at read time / in the preview).
102
- const linkInternalOpen = ref(false)
103
- const pickCollection = ref<string | null>(null)
104
- const pickRecordId = ref<number | null>(null)
105
- function toggleInternalLink() {
106
- linkOpen.value = false
107
- const opening = !linkInternalOpen.value
108
- linkInternalOpen.value = opening
109
- if (!opening) return
110
- // Prefill from the link under the cursor: an existing internal marker re-selects its record (the
111
- // picker loads + highlights it); an external/absent link opens the picker blank.
112
- const parsed = parseRichtextLinkHref(props.editor!.getAttributes('link').href as string | undefined)
113
- pickCollection.value = parsed?.collection ?? null
114
- pickRecordId.value = parsed?.id ?? null
115
- }
116
- function applyInternalLink() {
117
- const e = props.editor!
118
- if (pickCollection.value && pickRecordId.value != null) {
119
- e.chain().focus().extendMarkRange('link').setLink({ href: richtextLinkHref(pickCollection.value, pickRecordId.value) }).run()
120
- }
121
- linkInternalOpen.value = false
122
- pickCollection.value = null
123
- pickRecordId.value = null
124
- }
125
- </script>
126
-
127
- <template>
128
- <div v-if="editor" class="ui-rt-toolbar" :data-rev="tick">
129
- <UiSelect
130
- class="ui-rt-toolbar__block"
131
- :model-value="blockValue"
132
- :options="BLOCK_OPTIONS"
133
- :aria-label="t('richtext.blockType')"
134
- :disabled="disabled"
135
- @update:model-value="onBlock"
136
- />
137
-
138
- <div class="ui-rt-toolbar__group">
139
- <UiButton
140
- v-for="item in MARKS" :key="item.name"
141
- type="button" size="sm" variant="ghost"
142
- :class="{ 'rt-on': item.active!(editor) }"
143
- :aria-label="item.label"
144
- :aria-pressed="item.active!(editor) ? 'true' : 'false'"
145
- :disabled="disabled"
146
- @click="item.run(editor)"
147
- ><UiIcon :name="item.icon" :size="16" /></UiButton>
148
- </div>
149
-
150
- <div class="ui-rt-toolbar__group">
151
- <UiButton
152
- type="button" size="sm" variant="ghost"
153
- :class="{ 'rt-on': editor.isActive('link') }"
154
- :aria-label="t('richtext.link')"
155
- :aria-pressed="editor.isActive('link') ? 'true' : 'false'"
156
- :disabled="disabled"
157
- @click="toggleLink"
158
- ><UiIcon name="link" :size="16" /></UiButton>
159
- <UiButton
160
- type="button" size="sm" variant="ghost"
161
- :class="{ 'rt-on': linkInternalOpen }"
162
- :aria-label="t('richtext.linkInternal')"
163
- :aria-pressed="linkInternalOpen ? 'true' : 'false'"
164
- :disabled="disabled"
165
- @click="toggleInternalLink"
166
- ><UiIcon name="link-2" :size="16" /></UiButton>
167
- </div>
168
-
169
- <div class="ui-rt-toolbar__group">
170
- <UiButton
171
- v-for="item in BLOCKS" :key="item.name"
172
- type="button" size="sm" variant="ghost"
173
- :class="{ 'rt-on': item.active && item.active(editor) }"
174
- :aria-label="item.label"
175
- :aria-pressed="item.active ? (item.active(editor) ? 'true' : 'false') : undefined"
176
- :disabled="disabled"
177
- @click="item.run(editor)"
178
- ><UiIcon :name="item.icon" :size="16" /></UiButton>
179
- </div>
180
-
181
- <div class="ui-rt-toolbar__group">
182
- <UiButton
183
- v-for="item in ALIGN" :key="item.name"
184
- type="button" size="sm" variant="ghost"
185
- :class="{ 'rt-on': item.active!(editor) }"
186
- :aria-label="item.label"
187
- :aria-pressed="item.active!(editor) ? 'true' : 'false'"
188
- :disabled="disabled"
189
- @click="item.run(editor)"
190
- ><UiIcon :name="item.icon" :size="16" /></UiButton>
191
- </div>
192
-
193
- <div class="ui-rt-toolbar__group">
194
- <UiButton
195
- v-for="item in EXTRA" :key="item.name"
196
- type="button" size="sm" variant="ghost"
197
- :aria-label="item.label"
198
- :disabled="disabled || (item.disabled ? item.disabled(editor) : false)"
199
- @click="item.run(editor)"
200
- ><UiIcon :name="item.icon" :size="16" /></UiButton>
201
- </div>
202
-
203
- <div v-if="linkOpen" class="ui-rt-toolbar__link">
204
- <UiTextInput v-model="linkUrl" type="url" :placeholder="t('richtext.urlPlaceholder')" :aria-label="t('richtext.linkUrl')" @keydown.enter.prevent="applyLink" />
205
- <UiButton type="button" size="sm" variant="primary" @click="applyLink">{{ t('richtext.apply') }}</UiButton>
206
- </div>
207
-
208
- <div v-if="linkInternalOpen" class="ui-rt-toolbar__link">
209
- <LinkInternalPicker v-model:collection="pickCollection" v-model:record-id="pickRecordId" :locale="locale ?? 'en'" />
210
- <UiButton type="button" size="sm" variant="primary" :disabled="pickRecordId == null" @click="applyInternalLink">{{ t('richtext.apply') }}</UiButton>
211
- </div>
212
- </div>
213
- </template>
214
-
215
- <style lang="scss">
216
- .ui-rt-toolbar {
217
- display: flex;
218
- flex-wrap: wrap;
219
- align-items: center;
220
- gap: var(--space-2);
221
- padding: var(--space-2);
222
- border-bottom: 1px solid var(--color-border);
223
-
224
- &__group {
225
- display: inline-flex;
226
- gap: var(--space-1);
227
- }
228
- &__block {
229
- width: auto;
230
- min-width: 8rem;
231
- }
232
- &__link {
233
- display: flex;
234
- gap: var(--space-2);
235
- flex-basis: 100%;
236
- align-items: center;
237
- }
238
-
239
- &__group .ui-button.rt-on {
240
- background: var(--color-active, var(--color-surface-2));
241
- color: var(--color-text);
242
-
243
- &:hover:not(:disabled) {
244
- background: var(--color-active, var(--color-surface-2));
245
- }
246
- .ui-icon {
247
- color: var(--color-primary);
248
- }
249
- }
250
- }
251
- </style>
@@ -1,52 +0,0 @@
1
- <script setup lang="ts">
2
- import UiIcon from './Icon.vue'
3
-
4
- defineOptions({ inheritAttrs: false })
5
-
6
- withDefaults(
7
- defineProps<{
8
- options: { label: string; value: string }[]
9
- placeholder?: string
10
- disabled?: boolean
11
- }>(),
12
- { disabled: false },
13
- )
14
-
15
- const model = defineModel<string | null>()
16
- </script>
17
-
18
- <template>
19
- <span class="ui-select">
20
- <!-- eslint-disable-next-line vuejs-accessibility/form-control-has-label -- id/label land via v-bind="$attrs" from the wrapping UiField, invisible to static analysis -->
21
- <select v-model="model" :disabled="disabled" class="ui-select__field" v-bind="$attrs">
22
- <option v-if="placeholder" value="" disabled>{{ placeholder }}</option>
23
- <option v-for="o in options" :key="o.value" :value="o.value">{{ o.label }}</option>
24
- </select>
25
- <UiIcon name="chevron-down" :size="14" class="ui-select__icon" />
26
- </span>
27
- </template>
28
-
29
- <style lang="scss">
30
- @use '../../assets/scss/mixins';
31
-
32
- .ui-select {
33
- position: relative;
34
- display: block;
35
- width: 100%;
36
- }
37
- .ui-select__field {
38
- @include mixins.input-base;
39
- width: 100%;
40
- appearance: none;
41
- // leave room for the overlaid chevron so long values don't run under it
42
- padding-right: calc(var(--space-3) + 1.25rem);
43
- }
44
- .ui-select__icon {
45
- position: absolute;
46
- top: 50%;
47
- right: var(--space-3);
48
- transform: translateY(-50%);
49
- pointer-events: none; // clicks fall through to the native select
50
- color: var(--color-text-muted);
51
- }
52
- </style>