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