@michaelthielemann/kestrel 1.2.1

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 (395) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +12 -0
  3. package/README.md +234 -0
  4. package/kestrel.config.ts +28 -0
  5. package/layers/access/nuxt.config.ts +5 -0
  6. package/layers/access/server/middleware/00.ip-allowlist.ts +67 -0
  7. package/layers/access/server/middleware/access-guard.ts +30 -0
  8. package/layers/access/server/types.d.ts +8 -0
  9. package/layers/access/server/utils/csrf.ts +39 -0
  10. package/layers/access/server/utils/grant-registry.ts +36 -0
  11. package/layers/access/server/utils/guard.ts +44 -0
  12. package/layers/access/server/utils/ip-allowlist.ts +76 -0
  13. package/layers/access/server/utils/policy.ts +98 -0
  14. package/layers/access/server/utils/public-resources.ts +11 -0
  15. package/layers/access/server/utils/render-context.ts +41 -0
  16. package/layers/access/server/utils/require-admin.ts +15 -0
  17. package/layers/admin/app/components/AdminAccount.vue +212 -0
  18. package/layers/admin/app/components/AdminNav.vue +44 -0
  19. package/layers/admin/app/components/BlockFields.vue +68 -0
  20. package/layers/admin/app/components/BlockPreview.vue +401 -0
  21. package/layers/admin/app/components/BlockTree.vue +433 -0
  22. package/layers/admin/app/components/BlocksBody.vue +162 -0
  23. package/layers/admin/app/components/CollectionDeleteDialog.vue +38 -0
  24. package/layers/admin/app/components/CollectionEditor.vue +203 -0
  25. package/layers/admin/app/components/CollectionList.vue +1091 -0
  26. package/layers/admin/app/components/EditorStatus.vue +199 -0
  27. package/layers/admin/app/components/EditorUnsupported.vue +28 -0
  28. package/layers/admin/app/components/FieldsBody.vue +23 -0
  29. package/layers/admin/app/components/LocaleBar.vue +105 -0
  30. package/layers/admin/app/components/PageFields.vue +101 -0
  31. package/layers/admin/app/components/PageFieldsPane.vue +39 -0
  32. package/layers/admin/app/components/SeoFields.vue +169 -0
  33. package/layers/admin/app/components/SingletonEditor.vue +92 -0
  34. package/layers/admin/app/composables/useAuth.ts +68 -0
  35. package/layers/admin/app/composables/useBlockTree.ts +95 -0
  36. package/layers/admin/app/composables/useBlocks.ts +7 -0
  37. package/layers/admin/app/composables/useCachedResource.ts +35 -0
  38. package/layers/admin/app/composables/useCollectionOps.ts +53 -0
  39. package/layers/admin/app/composables/useCollections.ts +7 -0
  40. package/layers/admin/app/composables/useContentLocales.ts +23 -0
  41. package/layers/admin/app/composables/useEditForm.ts +370 -0
  42. package/layers/admin/app/composables/useLinkResolver.ts +40 -0
  43. package/layers/admin/app/composables/useListColumns.ts +53 -0
  44. package/layers/admin/app/composables/useListUrlState.ts +53 -0
  45. package/layers/admin/app/composables/usePendingPublishPoll.ts +15 -0
  46. package/layers/admin/app/composables/usePreviewViewport.ts +30 -0
  47. package/layers/admin/app/composables/usePublishStatus.ts +98 -0
  48. package/layers/admin/app/composables/useRailCollapsed.ts +14 -0
  49. package/layers/admin/app/composables/useTheme.ts +36 -0
  50. package/layers/admin/app/composables/useUnsavedGuard.ts +22 -0
  51. package/layers/admin/app/layouts/admin.vue +267 -0
  52. package/layers/admin/app/middleware/admin-auth.ts +4 -0
  53. package/layers/admin/app/pages/admin/[collection]/[id].vue +202 -0
  54. package/layers/admin/app/pages/admin/[collection]/index.vue +62 -0
  55. package/layers/admin/app/pages/admin/index.vue +118 -0
  56. package/layers/admin/app/pages/admin/login.vue +68 -0
  57. package/layers/admin/app/pages/admin/media.vue +28 -0
  58. package/layers/admin/app/pages/admin/references.vue +146 -0
  59. package/layers/admin/app/plugins/admin-style-guard.client.ts +76 -0
  60. package/layers/admin/app/plugins/reauth.client.ts +20 -0
  61. package/layers/admin/app/utils/admin-style-guard.ts +20 -0
  62. package/layers/admin/app/utils/block-tree.ts +214 -0
  63. package/layers/admin/app/utils/collection-ops.ts +44 -0
  64. package/layers/admin/app/utils/dead-refs.ts +30 -0
  65. package/layers/admin/app/utils/edit-form.ts +223 -0
  66. package/layers/admin/app/utils/editor-expose.ts +26 -0
  67. package/layers/admin/app/utils/editor-form-context.ts +83 -0
  68. package/layers/admin/app/utils/editor-registry.ts +20 -0
  69. package/layers/admin/app/utils/field-keys.ts +11 -0
  70. package/layers/admin/app/utils/list-columns.ts +141 -0
  71. package/layers/admin/app/utils/list-query.ts +96 -0
  72. package/layers/admin/app/utils/nav-active.ts +9 -0
  73. package/layers/admin/app/utils/populate-blocks.ts +288 -0
  74. package/layers/admin/app/utils/preview-channel.ts +62 -0
  75. package/layers/admin/app/utils/preview-viewport.ts +80 -0
  76. package/layers/admin/app/utils/reauth.ts +68 -0
  77. package/layers/admin/app/utils/record-title.ts +15 -0
  78. package/layers/admin/app/utils/register-builtin-editors.ts +9 -0
  79. package/layers/admin/app/utils/safe-redirect.ts +12 -0
  80. package/layers/admin/nuxt.config.ts +17 -0
  81. package/layers/auth/nuxt.config.ts +1 -0
  82. package/layers/auth/server/api/auth/login.post.ts +38 -0
  83. package/layers/auth/server/api/auth/logout.post.ts +9 -0
  84. package/layers/auth/server/api/auth/session.get.ts +1 -0
  85. package/layers/auth/server/utils/admin-credential.ts +42 -0
  86. package/layers/auth/server/utils/client-ip.ts +88 -0
  87. package/layers/auth/server/utils/login-throttle.ts +126 -0
  88. package/layers/auth/server/utils/password.ts +52 -0
  89. package/layers/auth/server/utils/session-cookie.ts +44 -0
  90. package/layers/auth/server/utils/session-epoch.ts +96 -0
  91. package/layers/auth/server/utils/session.ts +85 -0
  92. package/layers/collections/nuxt.config.ts +1 -0
  93. package/layers/collections/server/collections/pages.ts +19 -0
  94. package/layers/collections/server/plugins/01.register.ts +32 -0
  95. package/layers/collections/server/plugins/02.register-relation-populate.ts +17 -0
  96. package/layers/collections/server/utils/populate-relations.ts +66 -0
  97. package/layers/core/app/utils/bulk-actions.ts +9 -0
  98. package/layers/core/app/utils/filter-ops.ts +98 -0
  99. package/layers/core/app/utils/list-limits.ts +27 -0
  100. package/layers/core/app/utils/locale-path.ts +12 -0
  101. package/layers/core/app/utils/slugify.ts +16 -0
  102. package/layers/core/modules/auto-discovery/extract-block.ts +176 -0
  103. package/layers/core/modules/auto-discovery/index.ts +44 -0
  104. package/layers/core/modules/auto-discovery/scan.ts +78 -0
  105. package/layers/core/modules/auto-discovery/virtual.d.ts +12 -0
  106. package/layers/core/modules/kestrel/index.ts +86 -0
  107. package/layers/core/nuxt.config.ts +8 -0
  108. package/layers/core/server/api/[collection]/[id]/dead-refs.get.ts +8 -0
  109. package/layers/core/server/api/[collection]/[id]/translations.get.ts +4 -0
  110. package/layers/core/server/api/[collection]/[id].delete.ts +5 -0
  111. package/layers/core/server/api/[collection]/[id].get.ts +8 -0
  112. package/layers/core/server/api/[collection]/[id].patch.ts +8 -0
  113. package/layers/core/server/api/[collection]/bulk.post.ts +37 -0
  114. package/layers/core/server/api/[collection]/index.get.ts +19 -0
  115. package/layers/core/server/api/[collection]/index.post.ts +8 -0
  116. package/layers/core/server/api/[collection]/index.put.ts +7 -0
  117. package/layers/core/server/api/[collection]/options.get.ts +18 -0
  118. package/layers/core/server/api/[collection]/translations.get.ts +25 -0
  119. package/layers/core/server/api/collections/[name].get.ts +4 -0
  120. package/layers/core/server/api/collections/index.get.ts +3 -0
  121. package/layers/core/server/api/references/broken.get.ts +3 -0
  122. package/layers/core/server/api/references/referrers.get.ts +24 -0
  123. package/layers/core/server/database/record-refs.ts +20 -0
  124. package/layers/core/server/plugins/00.migrate.ts +11 -0
  125. package/layers/core/server/plugins/02.schema-sync.ts +63 -0
  126. package/layers/core/server/plugins/03.record-refs.ts +12 -0
  127. package/layers/core/server/schema/bootstrap.ts +41 -0
  128. package/layers/core/server/schema/desired.ts +99 -0
  129. package/layers/core/server/schema/dev-sync.ts +23 -0
  130. package/layers/core/server/schema/dialect.ts +59 -0
  131. package/layers/core/server/schema/diff.ts +110 -0
  132. package/layers/core/server/schema/introspect.ts +67 -0
  133. package/layers/core/server/schema/model.ts +61 -0
  134. package/layers/core/server/schema/render-sqlite.ts +89 -0
  135. package/layers/core/server/schema/sync.ts +201 -0
  136. package/layers/core/server/tasks/db/migrate.ts +54 -0
  137. package/layers/core/server/utils/blocks.ts +6 -0
  138. package/layers/core/server/utils/collection-types.ts +27 -0
  139. package/layers/core/server/utils/crud.ts +577 -0
  140. package/layers/core/server/utils/db.ts +26 -0
  141. package/layers/core/server/utils/defineCollection.ts +207 -0
  142. package/layers/core/server/utils/duplicate.ts +162 -0
  143. package/layers/core/server/utils/field-layout.ts +125 -0
  144. package/layers/core/server/utils/filter-predicate.ts +101 -0
  145. package/layers/core/server/utils/http.ts +50 -0
  146. package/layers/core/server/utils/if-unmodified.ts +13 -0
  147. package/layers/core/server/utils/kestrel-config.ts +410 -0
  148. package/layers/core/server/utils/key-lock.ts +33 -0
  149. package/layers/core/server/utils/locale.ts +53 -0
  150. package/layers/core/server/utils/page-route.ts +58 -0
  151. package/layers/core/server/utils/page-slug.ts +127 -0
  152. package/layers/core/server/utils/picker.ts +82 -0
  153. package/layers/core/server/utils/populate.ts +48 -0
  154. package/layers/core/server/utils/prerender-memo.ts +23 -0
  155. package/layers/core/server/utils/read-capture.ts +37 -0
  156. package/layers/core/server/utils/record-ref-index.ts +200 -0
  157. package/layers/core/server/utils/registry.ts +24 -0
  158. package/layers/core/server/utils/resolve-scope.ts +79 -0
  159. package/layers/core/server/utils/seo.ts +24 -0
  160. package/layers/core/server/utils/serialize-collection.ts +149 -0
  161. package/layers/core/server/utils/server-config.ts +18 -0
  162. package/layers/core/server/utils/sql.ts +2 -0
  163. package/layers/core/server/utils/storage.local.ts +117 -0
  164. package/layers/core/server/utils/storage.s3.ts +197 -0
  165. package/layers/core/server/utils/storage.ts +90 -0
  166. package/layers/core/server/utils/write-events.ts +31 -0
  167. package/layers/fields/app/utils/condition.ts +71 -0
  168. package/layers/fields/app/utils/define-block.ts +13 -0
  169. package/layers/fields/app/utils/field-constraints.ts +17 -0
  170. package/layers/fields/app/utils/field-factories.ts +129 -0
  171. package/layers/fields/app/utils/field-meta.ts +33 -0
  172. package/layers/fields/app/utils/field-validate.ts +75 -0
  173. package/layers/fields/app/utils/field-value.ts +7 -0
  174. package/layers/fields/app/utils/richtext-links.ts +48 -0
  175. package/layers/fields/nuxt.config.ts +1 -0
  176. package/layers/fields/server/api/blocks/index.get.ts +9 -0
  177. package/layers/fields/server/field-registry/index.ts +248 -0
  178. package/layers/fields/server/field-registry/naming.ts +10 -0
  179. package/layers/fields/server/field-registry/sanitize.ts +40 -0
  180. package/layers/fields/server/field-registry/types.ts +19 -0
  181. package/layers/fields/server/plugins/01.register-field-populate.ts +11 -0
  182. package/layers/fields/server/utils/block-ids.ts +43 -0
  183. package/layers/fields/server/utils/block-populate.ts +41 -0
  184. package/layers/fields/server/utils/buildCollection.ts +88 -0
  185. package/layers/fields/server/utils/buildFieldSchema.ts +22 -0
  186. package/layers/fields/server/utils/buildTable.ts +107 -0
  187. package/layers/fields/server/utils/conditional-required.ts +25 -0
  188. package/layers/fields/server/utils/defineBlock.ts +86 -0
  189. package/layers/fields/server/utils/defineFieldType.ts +33 -0
  190. package/layers/fields/server/utils/extract-refs.ts +152 -0
  191. package/layers/fields/server/utils/field-populate.ts +68 -0
  192. package/layers/media/app/components/KestrelImg.vue +65 -0
  193. package/layers/media/app/components/MediaConflictDialog.vue +35 -0
  194. package/layers/media/app/components/MediaDeleteDialog.vue +52 -0
  195. package/layers/media/app/components/MediaGrid.vue +113 -0
  196. package/layers/media/app/components/MediaImage.vue +70 -0
  197. package/layers/media/app/components/MediaLibrary.vue +451 -0
  198. package/layers/media/app/components/MediaNewFolderDialog.vue +32 -0
  199. package/layers/media/app/components/MediaPathBar.vue +163 -0
  200. package/layers/media/app/components/MediaPicker.vue +19 -0
  201. package/layers/media/app/components/MediaRenameDialog.vue +32 -0
  202. package/layers/media/app/components/MediaTable.vue +75 -0
  203. package/layers/media/app/components/MediaThumb.vue +25 -0
  204. package/layers/media/app/components/MediaToolbar.vue +90 -0
  205. package/layers/media/app/components/MediaUploadDialog.vue +87 -0
  206. package/layers/media/app/components/MediaViewer.vue +88 -0
  207. package/layers/media/app/components/field/Media.vue +261 -0
  208. package/layers/media/app/composables/useBlurUp.ts +55 -0
  209. package/layers/media/app/composables/useMediaClipboard.ts +14 -0
  210. package/layers/media/app/composables/useMediaContextMenu.ts +56 -0
  211. package/layers/media/app/composables/useMediaDnd.ts +56 -0
  212. package/layers/media/app/composables/useMediaLibrary.ts +151 -0
  213. package/layers/media/app/composables/useMediaOps.ts +50 -0
  214. package/layers/media/app/composables/useMediaRelocate.ts +39 -0
  215. package/layers/media/app/composables/useMediaResolver.ts +50 -0
  216. package/layers/media/app/composables/useMediaUpload.ts +93 -0
  217. package/layers/media/app/composables/useMediaVariant.ts +34 -0
  218. package/layers/media/app/plugins/field-media.client.ts +6 -0
  219. package/layers/media/app/utils/dnd.ts +69 -0
  220. package/layers/media/app/utils/library.ts +79 -0
  221. package/layers/media/app/utils/ops.ts +62 -0
  222. package/layers/media/app/utils/picture-model.ts +125 -0
  223. package/layers/media/app/utils/thumbhash.ts +19 -0
  224. package/layers/media/app/utils/variant-name.ts +20 -0
  225. package/layers/media/nuxt.config.ts +3 -0
  226. package/layers/media/server/api/media/[id]/usages.get.ts +8 -0
  227. package/layers/media/server/api/media/[id].delete.ts +18 -0
  228. package/layers/media/server/api/media/[id].get.ts +10 -0
  229. package/layers/media/server/api/media/[id].patch.ts +41 -0
  230. package/layers/media/server/api/media/backfill.post.ts +18 -0
  231. package/layers/media/server/api/media/copy.post.ts +7 -0
  232. package/layers/media/server/api/media/delete.post.ts +13 -0
  233. package/layers/media/server/api/media/folders.post.ts +18 -0
  234. package/layers/media/server/api/media/index.get.ts +15 -0
  235. package/layers/media/server/api/media/index.post.ts +157 -0
  236. package/layers/media/server/api/media/library.get.ts +17 -0
  237. package/layers/media/server/api/media/move.post.ts +7 -0
  238. package/layers/media/server/api/media/rename.post.ts +7 -0
  239. package/layers/media/server/api/media/resolve.get.ts +13 -0
  240. package/layers/media/server/collections/media-settings.ts +25 -0
  241. package/layers/media/server/collections/media.ts +29 -0
  242. package/layers/media/server/database/folders.ts +10 -0
  243. package/layers/media/server/middleware/ondemand-variants.ts +37 -0
  244. package/layers/media/server/plugins/02.register-media.ts +19 -0
  245. package/layers/media/server/plugins/03.media-cleanup.ts +20 -0
  246. package/layers/media/server/plugins/04.variant-capture.ts +23 -0
  247. package/layers/media/server/schema-tables/folders.ts +4 -0
  248. package/layers/media/server/tasks/media/backfill.ts +18 -0
  249. package/layers/media/server/utils/backfill.ts +132 -0
  250. package/layers/media/server/utils/derive.ts +59 -0
  251. package/layers/media/server/utils/folder-paths.ts +42 -0
  252. package/layers/media/server/utils/folders.ts +12 -0
  253. package/layers/media/server/utils/library.ts +90 -0
  254. package/layers/media/server/utils/media-cleanup.ts +27 -0
  255. package/layers/media/server/utils/media-enabled.ts +23 -0
  256. package/layers/media/server/utils/media-ops.ts +118 -0
  257. package/layers/media/server/utils/media-write.ts +13 -0
  258. package/layers/media/server/utils/naming.ts +52 -0
  259. package/layers/media/server/utils/ondemand.ts +69 -0
  260. package/layers/media/server/utils/persist-upload.ts +62 -0
  261. package/layers/media/server/utils/populate.ts +47 -0
  262. package/layers/media/server/utils/record.ts +63 -0
  263. package/layers/media/server/utils/relocate-ops.ts +341 -0
  264. package/layers/media/server/utils/relocate-plan.ts +63 -0
  265. package/layers/media/server/utils/resolve.ts +86 -0
  266. package/layers/media/server/utils/sanitize-svg.ts +12 -0
  267. package/layers/media/server/utils/sniff.ts +68 -0
  268. package/layers/media/server/utils/storage-relocate.ts +139 -0
  269. package/layers/media/server/utils/translations.ts +19 -0
  270. package/layers/media/server/utils/usages.ts +66 -0
  271. package/layers/media/server/utils/variant-capture.ts +70 -0
  272. package/layers/media/server/utils/variants.ts +108 -0
  273. package/layers/public/app/app.vue +5 -0
  274. package/layers/public/app/components/BlockRenderer.vue +56 -0
  275. package/layers/public/app/components/KestrelLink.vue +26 -0
  276. package/layers/public/app/components/KestrelPreviewBridge.vue +133 -0
  277. package/layers/public/app/composables/public-page.ts +24 -0
  278. package/layers/public/app/error.vue +12 -0
  279. package/layers/public/app/layouts/default.vue +5 -0
  280. package/layers/public/app/pages/[...slug].vue +157 -0
  281. package/layers/public/app/pages/__kestrel/preview.vue +34 -0
  282. package/layers/public/app/utils/block-edit-context.ts +16 -0
  283. package/layers/public/app/utils/link-href.ts +46 -0
  284. package/layers/public/app/utils/page-head.ts +84 -0
  285. package/layers/public/app/utils/preview-protocol.ts +81 -0
  286. package/layers/public/app/utils/public-route.ts +46 -0
  287. package/layers/public/modules/deploy-output/deploy-output.ts +370 -0
  288. package/layers/public/modules/deploy-output/index.ts +82 -0
  289. package/layers/public/modules/prerender-routes/discover.ts +37 -0
  290. package/layers/public/modules/prerender-routes/index.ts +85 -0
  291. package/layers/public/modules/prerender-routes/routes.ts +16 -0
  292. package/layers/public/modules/prune-media/index.ts +48 -0
  293. package/layers/public/modules/prune-media/prune-media.ts +81 -0
  294. package/layers/public/nuxt.config.ts +11 -0
  295. package/layers/public/server/api/links/resolve.get.ts +19 -0
  296. package/layers/public/server/api/publish-status.get.ts +44 -0
  297. package/layers/public/server/api/route.get.ts +18 -0
  298. package/layers/public/server/database/publish-deps.ts +12 -0
  299. package/layers/public/server/database/publish-status.ts +19 -0
  300. package/layers/public/server/plugins/02.register-links.ts +17 -0
  301. package/layers/public/server/plugins/zz.publish.ts +63 -0
  302. package/layers/public/server/routes/llms.txt.get.ts +72 -0
  303. package/layers/public/server/routes/robots.txt.get.ts +8 -0
  304. package/layers/public/server/routes/sitemap.xml.get.ts +66 -0
  305. package/layers/public/server/schema-tables/publish-deps.ts +4 -0
  306. package/layers/public/server/schema-tables/publish-status.ts +4 -0
  307. package/layers/public/server/tasks/publish/run.ts +26 -0
  308. package/layers/public/server/utils/link-resolve.ts +35 -0
  309. package/layers/public/server/utils/llms.ts +39 -0
  310. package/layers/public/server/utils/page-resolve.ts +86 -0
  311. package/layers/public/server/utils/populate-links.ts +66 -0
  312. package/layers/public/server/utils/publish/coalesce.ts +22 -0
  313. package/layers/public/server/utils/publish/deps-persistence.ts +43 -0
  314. package/layers/public/server/utils/publish/deps.ts +87 -0
  315. package/layers/public/server/utils/publish/invalidation.ts +141 -0
  316. package/layers/public/server/utils/publish/publish-status.ts +54 -0
  317. package/layers/public/server/utils/publish/publisher.ts +291 -0
  318. package/layers/public/server/utils/publish/queue.ts +67 -0
  319. package/layers/public/server/utils/publish/route-for-record.ts +16 -0
  320. package/layers/public/server/utils/publish/route-keys.ts +13 -0
  321. package/layers/public/server/utils/site-url.ts +32 -0
  322. package/layers/public/server/utils/sitemap.ts +97 -0
  323. package/layers/ui/app/assets/scss/_base.scss +16 -0
  324. package/layers/ui/app/assets/scss/_datepicker.scss +82 -0
  325. package/layers/ui/app/assets/scss/_mixins.scss +50 -0
  326. package/layers/ui/app/assets/scss/_reset.scss +65 -0
  327. package/layers/ui/app/assets/scss/_tokens.scss +183 -0
  328. package/layers/ui/app/assets/scss/main.scss +4 -0
  329. package/layers/ui/app/components/field/Boolean.vue +30 -0
  330. package/layers/ui/app/components/field/Choice.vue +68 -0
  331. package/layers/ui/app/components/field/Datetime.vue +96 -0
  332. package/layers/ui/app/components/field/DeadRefNote.vue +28 -0
  333. package/layers/ui/app/components/field/Json.vue +63 -0
  334. package/layers/ui/app/components/field/Layout.vue +140 -0
  335. package/layers/ui/app/components/field/Link.vue +112 -0
  336. package/layers/ui/app/components/field/LinkInternalPicker.vue +95 -0
  337. package/layers/ui/app/components/field/Number.vue +30 -0
  338. package/layers/ui/app/components/field/Relation.vue +49 -0
  339. package/layers/ui/app/components/field/Renderer.vue +30 -0
  340. package/layers/ui/app/components/field/Repeater.vue +460 -0
  341. package/layers/ui/app/components/field/Richtext.vue +26 -0
  342. package/layers/ui/app/components/field/Slug.vue +66 -0
  343. package/layers/ui/app/components/field/Text.vue +35 -0
  344. package/layers/ui/app/components/field/Unsupported.vue +34 -0
  345. package/layers/ui/app/components/ui/Alert.vue +50 -0
  346. package/layers/ui/app/components/ui/Brand.vue +22 -0
  347. package/layers/ui/app/components/ui/Button.vue +116 -0
  348. package/layers/ui/app/components/ui/ButtonGroup.vue +96 -0
  349. package/layers/ui/app/components/ui/Checkbox.vue +52 -0
  350. package/layers/ui/app/components/ui/CheckboxGroup.vue +50 -0
  351. package/layers/ui/app/components/ui/Combobox.vue +343 -0
  352. package/layers/ui/app/components/ui/DatePicker.vue +92 -0
  353. package/layers/ui/app/components/ui/DateRangePicker.vue +100 -0
  354. package/layers/ui/app/components/ui/Dialog.vue +107 -0
  355. package/layers/ui/app/components/ui/EmptyState.vue +46 -0
  356. package/layers/ui/app/components/ui/Field.vue +57 -0
  357. package/layers/ui/app/components/ui/Fieldset.vue +64 -0
  358. package/layers/ui/app/components/ui/Icon.vue +44 -0
  359. package/layers/ui/app/components/ui/Menu.vue +54 -0
  360. package/layers/ui/app/components/ui/NumberInput.vue +173 -0
  361. package/layers/ui/app/components/ui/Richtext.vue +119 -0
  362. package/layers/ui/app/components/ui/RichtextToolbar.vue +253 -0
  363. package/layers/ui/app/components/ui/Select.vue +51 -0
  364. package/layers/ui/app/components/ui/TextInput.vue +110 -0
  365. package/layers/ui/app/components/ui/Textarea.vue +38 -0
  366. package/layers/ui/app/components/ui/TimeInput.vue +17 -0
  367. package/layers/ui/app/components/ui/Toasts.vue +103 -0
  368. package/layers/ui/app/components/ui/Tooltip.vue +45 -0
  369. package/layers/ui/app/composables/useAdminLang.ts +8 -0
  370. package/layers/ui/app/composables/useDragReorder.ts +41 -0
  371. package/layers/ui/app/composables/useEchoGuard.ts +28 -0
  372. package/layers/ui/app/composables/useLinkField.ts +70 -0
  373. package/layers/ui/app/composables/useRecordOptions.ts +84 -0
  374. package/layers/ui/app/composables/useRepeater.ts +99 -0
  375. package/layers/ui/app/composables/useT.ts +32 -0
  376. package/layers/ui/app/composables/useToast.ts +46 -0
  377. package/layers/ui/app/i18n/de.ts +406 -0
  378. package/layers/ui/app/i18n/en.ts +440 -0
  379. package/layers/ui/app/utils/boolean-field.ts +10 -0
  380. package/layers/ui/app/utils/field-component.ts +22 -0
  381. package/layers/ui/app/utils/field-empty.ts +21 -0
  382. package/layers/ui/app/utils/field-registry.ts +36 -0
  383. package/layers/ui/app/utils/humanize.ts +14 -0
  384. package/layers/ui/app/utils/icons.ts +147 -0
  385. package/layers/ui/app/utils/iso-date.ts +18 -0
  386. package/layers/ui/app/utils/localized.ts +13 -0
  387. package/layers/ui/app/utils/record-context.ts +15 -0
  388. package/layers/ui/app/utils/reorder.ts +8 -0
  389. package/layers/ui/app/utils/richtext-preserve-class.ts +31 -0
  390. package/layers/ui/app/utils/useFieldA11y.ts +40 -0
  391. package/layers/ui/nuxt.config.ts +5 -0
  392. package/nuxt.config.ts +105 -0
  393. package/package.json +122 -0
  394. package/scripts/hash-password.mjs +26 -0
  395. package/scripts/typecheck.mjs +51 -0
@@ -0,0 +1,92 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import UiIcon from './Icon.vue'
4
+ import {
5
+ DatePickerRoot, DatePickerField, DatePickerInput, DatePickerTrigger,
6
+ DatePickerContent, DatePickerArrow, DatePickerCalendar,
7
+ DatePickerHeader, DatePickerPrev, DatePickerHeading, DatePickerNext,
8
+ DatePickerGrid, DatePickerGridHead, DatePickerGridBody, DatePickerGridRow,
9
+ DatePickerHeadCell, DatePickerCell, DatePickerCellTrigger,
10
+ } from 'reka-ui'
11
+ import type { DateValue } from '@internationalized/date'
12
+ import { isoToDate, dateToIso, type IsoPrecision } from '../../utils/iso-date'
13
+
14
+ const { t } = useT()
15
+
16
+ const props = withDefaults(
17
+ defineProps<{
18
+ precision?: IsoPrecision
19
+ disabled?: boolean
20
+ locale?: string
21
+ // a11y: the segmented control has no single labelable <input>, so the field name is carried as an
22
+ // aria-label on the role=group field, with the error/hint + invalid/required state wired alongside.
23
+ ariaLabel?: string
24
+ describedby?: string
25
+ invalid?: boolean
26
+ required?: boolean
27
+ }>(),
28
+ { precision: 'datetime', disabled: false },
29
+ )
30
+ const model = defineModel<string | null>()
31
+
32
+ // Match the native inputs, which format to the browser locale. Overridable for a future admin-locale setting.
33
+ const displayLocale = computed(() => props.locale ?? (typeof navigator !== 'undefined' ? navigator.language : 'en'))
34
+ const granularity = computed(() => (props.precision === 'date' ? 'day' : 'minute'))
35
+ const value = computed<DateValue | undefined>({
36
+ get: () => isoToDate(model.value, props.precision),
37
+ set: (v) => { model.value = dateToIso(v) },
38
+ })
39
+ </script>
40
+
41
+ <template>
42
+ <DatePickerRoot v-model="value" :granularity="granularity" :disabled="disabled" :locale="displayLocale">
43
+ <DatePickerField
44
+ v-slot="{ segments }"
45
+ class="ui-datepicker__field"
46
+ role="group"
47
+ :aria-label="ariaLabel"
48
+ :aria-describedby="describedby"
49
+ :aria-invalid="invalid || undefined"
50
+ :aria-required="required || undefined"
51
+ >
52
+ <DatePickerInput
53
+ v-for="item in segments"
54
+ :key="item.part"
55
+ :part="item.part"
56
+ class="ui-datepicker__segment"
57
+ >{{ item.value }}</DatePickerInput>
58
+ <DatePickerTrigger class="ui-datepicker__trigger" :aria-label="t('datePicker.openCalendar')"><UiIcon name="calendar" :size="16" /></DatePickerTrigger>
59
+ </DatePickerField>
60
+
61
+ <DatePickerContent class="ui-datepicker__content" :side-offset="6">
62
+ <DatePickerArrow class="ui-datepicker__arrow" />
63
+ <DatePickerCalendar v-slot="{ weekDays, grid }" class="ui-datepicker__calendar">
64
+ <DatePickerHeader class="ui-datepicker__header">
65
+ <DatePickerPrev class="ui-datepicker__nav" :aria-label="t('datePicker.previousMonth')">‹</DatePickerPrev>
66
+ <DatePickerHeading class="ui-datepicker__heading" />
67
+ <DatePickerNext class="ui-datepicker__nav" :aria-label="t('datePicker.nextMonth')">›</DatePickerNext>
68
+ </DatePickerHeader>
69
+ <DatePickerGrid v-for="month in grid" :key="month.value.toString()" class="ui-datepicker__grid">
70
+ <DatePickerGridHead>
71
+ <DatePickerGridRow class="ui-datepicker__row">
72
+ <DatePickerHeadCell v-for="day in weekDays" :key="day" class="ui-datepicker__weekday">{{ day }}</DatePickerHeadCell>
73
+ </DatePickerGridRow>
74
+ </DatePickerGridHead>
75
+ <DatePickerGridBody>
76
+ <DatePickerGridRow v-for="(weekDates, i) in month.rows" :key="`w${i}`" class="ui-datepicker__row">
77
+ <DatePickerCell v-for="weekDate in weekDates" :key="weekDate.toString()" :date="weekDate">
78
+ <DatePickerCellTrigger :day="weekDate" :month="month.value" class="ui-datepicker__cell" />
79
+ </DatePickerCell>
80
+ </DatePickerGridRow>
81
+ </DatePickerGridBody>
82
+ </DatePickerGrid>
83
+ </DatePickerCalendar>
84
+ </DatePickerContent>
85
+ </DatePickerRoot>
86
+ </template>
87
+
88
+ <style lang="scss">
89
+ // Shared, global (non-scoped) datepicker chrome — the calendar is teleported to <body>.
90
+ @use '../../assets/scss/datepicker' as *;
91
+ @include ui-datepicker;
92
+ </style>
@@ -0,0 +1,100 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import UiIcon from './Icon.vue'
4
+ import {
5
+ DateRangePickerRoot, DateRangePickerField, DateRangePickerInput, DateRangePickerTrigger,
6
+ DateRangePickerContent, DateRangePickerArrow, DateRangePickerCalendar,
7
+ DateRangePickerHeader, DateRangePickerPrev, DateRangePickerHeading, DateRangePickerNext,
8
+ DateRangePickerGrid, DateRangePickerGridHead, DateRangePickerGridBody, DateRangePickerGridRow,
9
+ DateRangePickerHeadCell, DateRangePickerCell, DateRangePickerCellTrigger,
10
+ } from 'reka-ui'
11
+ import type { DateValue } from '@internationalized/date'
12
+ import { isoToDate, dateToIso, type IsoPrecision } from '../../utils/iso-date'
13
+
14
+ const { t } = useT()
15
+ const props = withDefaults(
16
+ defineProps<{
17
+ precision?: IsoPrecision
18
+ disabled?: boolean
19
+ locale?: string
20
+ // a11y: see DatePicker — the field name rides as an aria-label on the role=group field.
21
+ ariaLabel?: string
22
+ describedby?: string
23
+ invalid?: boolean
24
+ required?: boolean
25
+ }>(),
26
+ { precision: 'datetime', disabled: false },
27
+ )
28
+ const model = defineModel<{ start: string; end: string } | null>()
29
+
30
+ const displayLocale = computed(() => props.locale ?? (typeof navigator !== 'undefined' ? navigator.language : 'en'))
31
+ const granularity = computed(() => (props.precision === 'date' ? 'day' : 'minute'))
32
+ const value = computed<{ start: DateValue | undefined; end: DateValue | undefined }>({
33
+ get: () => ({
34
+ start: isoToDate(model.value?.start, props.precision),
35
+ end: isoToDate(model.value?.end, props.precision),
36
+ }),
37
+ set: (v) => {
38
+ const start = dateToIso(v.start)
39
+ const end = dateToIso(v.end)
40
+ // Preserve a partial selection (Reka commits start before end) as empty strings.
41
+ model.value = start || end ? { start: start ?? '', end: end ?? '' } : null
42
+ },
43
+ })
44
+ </script>
45
+
46
+ <template>
47
+ <DateRangePickerRoot v-model="value" :granularity="granularity" :disabled="disabled" :locale="displayLocale">
48
+ <DateRangePickerField
49
+ v-slot="{ segments }"
50
+ class="ui-datepicker__field"
51
+ role="group"
52
+ :aria-label="ariaLabel"
53
+ :aria-describedby="describedby"
54
+ :aria-invalid="invalid || undefined"
55
+ :aria-required="required || undefined"
56
+ >
57
+ <template v-for="item in segments.start" :key="`s-${item.part}`">
58
+ <DateRangePickerInput :part="item.part" type="start" class="ui-datepicker__segment">{{ item.value }}</DateRangePickerInput>
59
+ </template>
60
+ <span class="ui-datepicker__sep" aria-hidden="true">–</span>
61
+ <template v-for="item in segments.end" :key="`e-${item.part}`">
62
+ <DateRangePickerInput :part="item.part" type="end" class="ui-datepicker__segment">{{ item.value }}</DateRangePickerInput>
63
+ </template>
64
+ <DateRangePickerTrigger class="ui-datepicker__trigger" :aria-label="t('dateRange.openCalendar')"><UiIcon name="calendar" :size="16" /></DateRangePickerTrigger>
65
+ </DateRangePickerField>
66
+
67
+ <DateRangePickerContent class="ui-datepicker__content" :side-offset="6">
68
+ <DateRangePickerArrow class="ui-datepicker__arrow" />
69
+ <DateRangePickerCalendar v-slot="{ weekDays, grid }" class="ui-datepicker__calendar">
70
+ <DateRangePickerHeader class="ui-datepicker__header">
71
+ <DateRangePickerPrev class="ui-datepicker__nav" :aria-label="t('dateRange.previousMonth')">‹</DateRangePickerPrev>
72
+ <DateRangePickerHeading class="ui-datepicker__heading" />
73
+ <DateRangePickerNext class="ui-datepicker__nav" :aria-label="t('dateRange.nextMonth')">›</DateRangePickerNext>
74
+ </DateRangePickerHeader>
75
+ <DateRangePickerGrid v-for="month in grid" :key="month.value.toString()" class="ui-datepicker__grid">
76
+ <DateRangePickerGridHead>
77
+ <DateRangePickerGridRow class="ui-datepicker__row">
78
+ <DateRangePickerHeadCell v-for="day in weekDays" :key="day" class="ui-datepicker__weekday">{{ day }}</DateRangePickerHeadCell>
79
+ </DateRangePickerGridRow>
80
+ </DateRangePickerGridHead>
81
+ <DateRangePickerGridBody>
82
+ <DateRangePickerGridRow v-for="(weekDates, i) in month.rows" :key="`w${i}`" class="ui-datepicker__row">
83
+ <DateRangePickerCell v-for="weekDate in weekDates" :key="weekDate.toString()" :date="weekDate">
84
+ <DateRangePickerCellTrigger :day="weekDate" :month="month.value" class="ui-datepicker__cell" />
85
+ </DateRangePickerCell>
86
+ </DateRangePickerGridRow>
87
+ </DateRangePickerGridBody>
88
+ </DateRangePickerGrid>
89
+ </DateRangePickerCalendar>
90
+ </DateRangePickerContent>
91
+ </DateRangePickerRoot>
92
+ </template>
93
+
94
+ <style lang="scss">
95
+ // Shares the global (non-scoped) datepicker chrome — the calendar is teleported to <body>, so a
96
+ // `scoped` block would silently drop these styles. Included explicitly rather than borrowing them
97
+ // from DatePicker being present on the page.
98
+ @use '../../assets/scss/datepicker' as *;
99
+ @include ui-datepicker;
100
+ </style>
@@ -0,0 +1,107 @@
1
+ <script setup lang="ts">
2
+ import { DialogRoot, DialogOverlay, DialogContent, DialogTitle, DialogDescription, DialogClose } from 'reka-ui'
3
+ import UiIcon from './Icon.vue'
4
+
5
+ const props = defineProps<{ open: boolean; title: string; description?: string; size?: 'md' | 'lg' | 'xl' }>()
6
+ const emit = defineEmits<{ 'update:open': [boolean] }>()
7
+ const { t } = useT()
8
+
9
+ // Rendering in place means dialog inputs are form-associated with any ancestor form (the record
10
+ // editor): Enter in a text-entry input would implicitly submit it — saving the half-edited record
11
+ // from inside a picker. Swallow that default here; Enter on buttons (and click-like inputs) keeps
12
+ // its activation behavior, and a dialog can still opt into Enter via its own @keydown.enter handler.
13
+ const CLICK_INPUT_TYPES = new Set(['button', 'submit', 'reset', 'checkbox', 'radio', 'file'])
14
+ function onKeydown(e: KeyboardEvent) {
15
+ if (e.key !== 'Enter' || e.isComposing) return
16
+ const el = e.target
17
+ if (el instanceof HTMLInputElement && !CLICK_INPUT_TYPES.has(el.type)) e.preventDefault()
18
+ }
19
+ </script>
20
+
21
+ <template>
22
+ <!-- Reka Dialog, rendered IN PLACE (no DialogPortal). Reka provides the focus trap, scroll lock,
23
+ Escape/outside-click dismissal, ARIA wiring and focus restore; rendering in place (rather than
24
+ teleporting) keeps the dialog synchronously DOM-testable and needs no force-mount hack. The
25
+ admin's flat layouts don't need a portal to escape clipping — add DialogPortal here if that
26
+ ever changes. -->
27
+ <DialogRoot :open="props.open" @update:open="emit('update:open', $event)">
28
+ <DialogOverlay class="ui-dialog__overlay" />
29
+ <DialogContent
30
+ class="ui-dialog__content"
31
+ :class="`ui-dialog__content--${size ?? 'md'}`"
32
+ v-bind="description ? {} : { 'aria-describedby': undefined }"
33
+ @keydown="onKeydown"
34
+ >
35
+ <header class="ui-dialog__header">
36
+ <DialogTitle class="ui-dialog__title">{{ title }}</DialogTitle>
37
+ <DialogClose data-test="dialog-close" class="ui-dialog__close" :aria-label="t('common.close')"><UiIcon name="x" :size="16" /></DialogClose>
38
+ </header>
39
+ <DialogDescription v-if="description" class="ui-dialog__desc">{{ description }}</DialogDescription>
40
+ <div class="ui-dialog__body"><slot /></div>
41
+ <footer v-if="$slots.footer" class="ui-dialog__footer"><slot name="footer" /></footer>
42
+ </DialogContent>
43
+ </DialogRoot>
44
+ </template>
45
+
46
+ <style lang="scss" scoped>
47
+ .ui-dialog__overlay {
48
+ position: fixed;
49
+ inset: 0;
50
+ z-index: var(--z-dialog);
51
+ background: var(--color-scrim);
52
+ }
53
+ .ui-dialog__content {
54
+ position: fixed;
55
+ top: 50%;
56
+ left: 50%;
57
+ transform: translate(-50%, -50%);
58
+ z-index: var(--z-dialog);
59
+ max-height: calc(100svh - var(--space-6));
60
+ overflow: auto;
61
+ background: var(--color-surface);
62
+ border: 1px solid var(--color-border);
63
+ border-radius: var(--radius-lg);
64
+ box-shadow: var(--shadow-lg);
65
+ padding: var(--space-4);
66
+
67
+ &:focus,
68
+ &:focus-visible { outline: none; }
69
+ }
70
+ .ui-dialog__content--md { width: min(32rem, calc(100vw - 2rem)); }
71
+ .ui-dialog__content--lg { width: min(64rem, calc(100vw - 2rem)); }
72
+ .ui-dialog__content--xl { width: min(80rem, calc(100vw - 2rem)); }
73
+ .ui-dialog__header {
74
+ display: flex;
75
+ align-items: center;
76
+ justify-content: space-between;
77
+ gap: var(--space-2);
78
+ margin-bottom: var(--space-3);
79
+ }
80
+ .ui-dialog__title {
81
+ font-size: var(--text-lg);
82
+ font-weight: var(--weight-bold);
83
+ margin: 0;
84
+ }
85
+ .ui-dialog__desc {
86
+ margin: 0 0 var(--space-3);
87
+ color: var(--color-text-muted);
88
+ }
89
+ .ui-dialog__close {
90
+ background: none;
91
+ border: 0;
92
+ font-size: var(--text-xl);
93
+ line-height: 1;
94
+ cursor: pointer;
95
+ color: var(--color-text-muted);
96
+ border-radius: var(--radius-sm);
97
+
98
+ &:hover { background: var(--color-hover); color: var(--color-text); }
99
+ &:focus-visible { outline: 2px solid var(--color-focus); outline-offset: -2px; }
100
+ }
101
+ .ui-dialog__footer {
102
+ display: flex;
103
+ justify-content: flex-end;
104
+ gap: var(--space-2);
105
+ margin-top: var(--space-4);
106
+ }
107
+ </style>
@@ -0,0 +1,46 @@
1
+ <script setup lang="ts">
2
+ import type { IconName } from '../../utils/icons'
3
+
4
+ defineProps<{ icon?: IconName; title: string; description?: string }>()
5
+ </script>
6
+
7
+ <template>
8
+ <div class="ui-empty">
9
+ <UiIcon v-if="icon" :name="icon" size="1.5rem" class="ui-empty__icon" />
10
+ <p class="ui-empty__title">{{ title }}</p>
11
+ <p v-if="description" class="ui-empty__desc">{{ description }}</p>
12
+ <div v-if="$slots.action" class="ui-empty__action"><slot name="action" /></div>
13
+ </div>
14
+ </template>
15
+
16
+ <style lang="scss">
17
+ .ui-empty {
18
+ display: flex;
19
+ flex-direction: column;
20
+ align-items: center;
21
+ justify-content: center;
22
+ gap: var(--space-2);
23
+ padding: var(--space-6) var(--space-4);
24
+ text-align: center;
25
+ border: 1px solid var(--color-border);
26
+ border-radius: var(--radius-lg);
27
+ background: var(--color-surface);
28
+
29
+ &__icon {
30
+ margin-bottom: var(--space-1);
31
+ color: var(--color-text-subtle);
32
+ }
33
+ &__title {
34
+ font-weight: var(--weight-medium);
35
+ color: var(--color-text);
36
+ }
37
+ &__desc {
38
+ font-size: var(--text-sm);
39
+ color: var(--color-text-muted);
40
+ max-width: 40ch;
41
+ }
42
+ &__action {
43
+ margin-top: var(--space-2);
44
+ }
45
+ }
46
+ </style>
@@ -0,0 +1,57 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { useFieldA11y } from '../../utils/useFieldA11y'
4
+
5
+ const props = defineProps<{
6
+ label?: string
7
+ hint?: string
8
+ error?: string | null
9
+ required?: boolean
10
+ id?: string
11
+ }>()
12
+
13
+ const { fieldId, hintId, errId, describedby, ariaInvalid } = useFieldA11y(props)
14
+
15
+ const slotProps = computed(() => ({
16
+ id: fieldId.value,
17
+ 'aria-invalid': ariaInvalid.value,
18
+ 'aria-describedby': describedby.value,
19
+ required: props.required,
20
+ }))
21
+ </script>
22
+
23
+ <template>
24
+ <div class="ui-field" :data-invalid="error ? 'true' : undefined">
25
+ <label v-if="label" :for="fieldId" class="ui-field__label">
26
+ {{ label }}<span v-if="required" class="ui-field__required" aria-hidden="true">*</span>
27
+ </label>
28
+ <slot v-bind="slotProps" />
29
+ <p v-if="hint" :id="hintId" class="ui-field__hint">{{ hint }}</p>
30
+ <p v-if="error" :id="errId" class="ui-field__error" role="alert">{{ error }}</p>
31
+ </div>
32
+ </template>
33
+
34
+ <style lang="scss">
35
+ .ui-field {
36
+ display: flex;
37
+ flex-direction: column;
38
+ gap: var(--space-1);
39
+
40
+ &__label {
41
+ font-size: var(--text-sm);
42
+ font-weight: var(--weight-medium);
43
+ }
44
+ &__required {
45
+ color: var(--color-danger);
46
+ margin-inline-start: var(--space-1);
47
+ }
48
+ &__hint {
49
+ font-size: var(--text-sm);
50
+ color: var(--color-text-muted);
51
+ }
52
+ &__error {
53
+ font-size: var(--text-sm);
54
+ color: var(--color-danger);
55
+ }
56
+ }
57
+ </style>
@@ -0,0 +1,64 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { useFieldA11y } from '../../utils/useFieldA11y'
4
+
5
+ const props = defineProps<{
6
+ label?: string
7
+ hint?: string
8
+ error?: string | null
9
+ required?: boolean
10
+ id?: string
11
+ }>()
12
+
13
+ const { fieldId, hintId, errId, describedby, ariaInvalid, ariaRequired } = useFieldA11y(props)
14
+
15
+ // No `id` — a group has no single control to point a label `for` at; the
16
+ // <legend> labels the group natively. Consumers spread these onto the group root.
17
+ const slotProps = computed(() => ({
18
+ 'aria-invalid': ariaInvalid.value,
19
+ 'aria-describedby': describedby.value,
20
+ 'aria-required': ariaRequired.value,
21
+ required: props.required,
22
+ }))
23
+ </script>
24
+
25
+ <template>
26
+ <fieldset class="ui-fieldset" :data-invalid="error ? 'true' : undefined">
27
+ <legend v-if="label" class="ui-fieldset__legend">
28
+ {{ label }}<span v-if="required" class="ui-fieldset__required" aria-hidden="true">*</span>
29
+ </legend>
30
+ <slot v-bind="slotProps" />
31
+ <p v-if="hint" :id="hintId" class="ui-fieldset__hint">{{ hint }}</p>
32
+ <p v-if="error" :id="errId" class="ui-fieldset__error" role="alert">{{ error }}</p>
33
+ </fieldset>
34
+ </template>
35
+
36
+ <style lang="scss">
37
+ .ui-fieldset {
38
+ display: flex;
39
+ flex-direction: column;
40
+ gap: var(--space-1);
41
+ min-inline-size: 0;
42
+ margin: 0;
43
+ padding: 0;
44
+ border: 0;
45
+
46
+ &__legend {
47
+ padding: 0;
48
+ font-size: var(--text-sm);
49
+ font-weight: var(--weight-medium);
50
+ }
51
+ &__required {
52
+ color: var(--color-danger);
53
+ margin-inline-start: var(--space-1);
54
+ }
55
+ &__hint {
56
+ font-size: var(--text-sm);
57
+ color: var(--color-text-muted);
58
+ }
59
+ &__error {
60
+ font-size: var(--text-sm);
61
+ color: var(--color-danger);
62
+ }
63
+ }
64
+ </style>
@@ -0,0 +1,44 @@
1
+ <script setup lang="ts">
2
+ import { computed, useAttrs } from 'vue'
3
+ import { isIconName, resolveIconBody, type IconSpec } from '../../utils/icons'
4
+
5
+ const props = withDefaults(defineProps<{ name: IconSpec; size?: string | number; label?: string }>(), { size: '1em' })
6
+ const attrs = useAttrs()
7
+
8
+ const dim = computed(() => (typeof props.size === 'number' ? `${props.size}px` : props.size))
9
+ const body = computed(() => resolveIconBody(props.name))
10
+ // `data-icon` exposes the resolved registry key (or `custom` for raw SVG) for styling/tests.
11
+ const dataIcon = computed(() => (isIconName(props.name) ? props.name : 'custom'))
12
+ // A caller can give the icon an accessible name via fallthrough attrs (role/aria-label) instead of the
13
+ // `label` prop — either way it must not stay `aria-hidden`, or the name is invisible to AT.
14
+ const hasA11yName = computed(() => props.label != null || attrs.role != null || attrs['aria-label'] != null || attrs['aria-labelledby'] != null)
15
+ </script>
16
+
17
+ <template>
18
+ <!-- v-html body is either a static registry icon or developer-supplied SVG allowlist-sanitised by
19
+ resolveIconBody → sanitizeIconSvg (no script-capable elements/attributes survive). -->
20
+ <svg
21
+ class="ui-icon"
22
+ :data-icon="dataIcon"
23
+ :width="dim"
24
+ :height="dim"
25
+ viewBox="0 0 24 24"
26
+ fill="none"
27
+ stroke="currentColor"
28
+ stroke-width="2"
29
+ stroke-linecap="round"
30
+ stroke-linejoin="round"
31
+ :role="label ? 'img' : undefined"
32
+ :aria-label="label || undefined"
33
+ :aria-hidden="hasA11yName ? undefined : 'true'"
34
+ v-html="body"
35
+ />
36
+ </template>
37
+
38
+ <style lang="scss">
39
+ .ui-icon {
40
+ display: inline-block;
41
+ vertical-align: middle;
42
+ flex-shrink: 0;
43
+ }
44
+ </style>
@@ -0,0 +1,54 @@
1
+ <script setup lang="ts">
2
+ import { ContextMenuRoot, ContextMenuTrigger, ContextMenuPortal, ContextMenuContent, ContextMenuItem } from 'reka-ui'
3
+
4
+ export interface MenuItem { label: string; value: string; danger?: boolean; disabled?: boolean }
5
+ defineProps<{ items: MenuItem[] }>()
6
+ const emit = defineEmits<{ select: [string] }>()
7
+ </script>
8
+
9
+ <template>
10
+ <ContextMenuRoot>
11
+ <ContextMenuTrigger as-child>
12
+ <slot />
13
+ </ContextMenuTrigger>
14
+ <ContextMenuPortal>
15
+ <ContextMenuContent class="ui-menu" :collision-padding="8">
16
+ <ContextMenuItem
17
+ v-for="it in items"
18
+ :key="it.value"
19
+ :disabled="it.disabled"
20
+ class="ui-menu__item"
21
+ :class="{ 'ui-menu__item--danger': it.danger }"
22
+ @select="emit('select', it.value)"
23
+ >{{ it.label }}</ContextMenuItem>
24
+ </ContextMenuContent>
25
+ </ContextMenuPortal>
26
+ </ContextMenuRoot>
27
+ </template>
28
+
29
+ <style lang="scss">
30
+ // Not scoped: Reka teleports the menu content to <body>, where a scoped data-v selector would no
31
+ // longer match — so the surface background/border silently dropped out (Image: a backgroundless,
32
+ // hard-to-read context menu). The other portaled overlays (Combobox/DatePicker/Select) are global too.
33
+ .ui-menu {
34
+ min-width: 11rem;
35
+ background: var(--color-surface);
36
+ border: 1px solid var(--color-border);
37
+ border-radius: var(--radius-md);
38
+ box-shadow: var(--shadow-md);
39
+ padding: var(--space-1);
40
+ z-index: var(--z-dropdown);
41
+ }
42
+ .ui-menu__item {
43
+ display: block;
44
+ width: 100%;
45
+ padding: var(--space-1) var(--space-3);
46
+ border-radius: var(--radius-sm);
47
+ font-size: var(--text-sm);
48
+ cursor: pointer;
49
+ user-select: none;
50
+ &[data-highlighted] { background: var(--color-hover); outline: none; }
51
+ &[data-disabled] { color: var(--color-text-muted); cursor: default; }
52
+ &--danger { color: var(--color-danger); }
53
+ }
54
+ </style>