@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,116 @@
1
+ <script setup lang="ts">
2
+ import type { IconName } from '../../utils/icons'
3
+
4
+ withDefaults(
5
+ defineProps<{
6
+ variant?: 'primary' | 'secondary' | 'ghost' | 'danger'
7
+ size?: 'sm' | 'md' | 'lg'
8
+ type?: 'button' | 'submit' | 'reset'
9
+ disabled?: boolean
10
+ loading?: boolean
11
+ icon?: IconName
12
+ }>(),
13
+ { variant: 'secondary', size: 'md', type: 'button', disabled: false, loading: false },
14
+ )
15
+ </script>
16
+
17
+ <template>
18
+ <button
19
+ :type="type"
20
+ class="ui-button"
21
+ :class="[`ui-button--${variant}`, `ui-button--${size}`]"
22
+ :disabled="disabled || loading"
23
+ :aria-busy="loading || undefined"
24
+ >
25
+ <span v-if="loading" class="ui-button__spinner" aria-hidden="true" />
26
+ <UiIcon v-else-if="icon" :name="icon" class="ui-button__icon" />
27
+ <span v-if="$slots.default" class="ui-button__label"><slot /></span>
28
+ </button>
29
+ </template>
30
+
31
+ <style lang="scss">
32
+ @use '../../assets/scss/mixins' as *;
33
+
34
+ .ui-button {
35
+ @include focus-ring;
36
+ display: inline-flex;
37
+ align-items: center;
38
+ justify-content: center;
39
+ gap: var(--space-2);
40
+ border: 1px solid transparent;
41
+ border-radius: var(--radius-md);
42
+ font-weight: var(--weight-medium);
43
+ line-height: 1;
44
+ transition:
45
+ background-color var(--motion-fast) var(--ease-standard),
46
+ border-color var(--motion-fast) var(--ease-standard);
47
+
48
+ &:disabled {
49
+ opacity: 0.6;
50
+ cursor: not-allowed;
51
+ }
52
+
53
+ &--sm {
54
+ padding: var(--space-1) var(--space-2);
55
+ font-size: var(--text-sm);
56
+ }
57
+ &--md {
58
+ padding: var(--space-2) var(--space-4);
59
+ font-size: var(--text-base);
60
+ }
61
+ &--lg {
62
+ padding: var(--space-3) var(--space-5);
63
+ font-size: var(--text-lg);
64
+ }
65
+
66
+ &--primary {
67
+ background: var(--color-primary);
68
+ color: var(--color-on-primary);
69
+
70
+ &:hover:not(:disabled) {
71
+ background: var(--color-primary-hover);
72
+ }
73
+ }
74
+ &--secondary {
75
+ background: var(--color-surface);
76
+ color: var(--color-text);
77
+ border-color: var(--color-border);
78
+
79
+ &:hover:not(:disabled) {
80
+ background: var(--color-surface-2);
81
+ border-color: var(--color-border-strong);
82
+ }
83
+ }
84
+ &--ghost {
85
+ background: transparent;
86
+ color: var(--color-text);
87
+
88
+ &:hover:not(:disabled) {
89
+ background: var(--color-hover);
90
+ }
91
+ }
92
+ &--danger {
93
+ background: var(--color-danger-solid);
94
+ color: var(--color-on-danger);
95
+
96
+ &:hover:not(:disabled) {
97
+ filter: brightness(0.93);
98
+ }
99
+ }
100
+
101
+ &__spinner {
102
+ width: 1em;
103
+ height: 1em;
104
+ border: 2px solid currentColor;
105
+ border-top-color: transparent;
106
+ border-radius: var(--radius-full);
107
+ animation: ui-button-spin var(--motion-base) linear infinite;
108
+ }
109
+ }
110
+
111
+ @keyframes ui-button-spin {
112
+ to {
113
+ transform: rotate(360deg);
114
+ }
115
+ }
116
+ </style>
@@ -0,0 +1,96 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { ToggleGroupRoot, ToggleGroupItem } from 'reka-ui'
4
+ import UiIcon from './Icon.vue'
5
+ import type { IconName } from '../../utils/icons'
6
+
7
+ const props = withDefaults(
8
+ defineProps<{
9
+ options: { label: string; value: string; icon?: IconName }[]
10
+ multiple?: boolean
11
+ disabled?: boolean
12
+ }>(),
13
+ { multiple: false, disabled: false },
14
+ )
15
+
16
+ const model = defineModel<string | string[] | null>()
17
+
18
+ const current = computed(() =>
19
+ props.multiple ? (Array.isArray(model.value) ? model.value : []) : (model.value ?? undefined),
20
+ )
21
+
22
+ // reka's ToggleGroupRoot emits the broad `AcceptableValue`; an `unknown` param satisfies that contract and
23
+ // we coerce back to our `string | string[]` model here.
24
+ function onUpdate(v: unknown) {
25
+ if (props.multiple) model.value = Array.isArray(v) ? (v as string[]) : []
26
+ else model.value = v ? (v as string) : null
27
+ }
28
+ </script>
29
+
30
+ <template>
31
+ <ToggleGroupRoot
32
+ :model-value="current"
33
+ :type="multiple ? 'multiple' : 'single'"
34
+ :disabled="disabled"
35
+ class="ui-btngroup"
36
+ @update:model-value="onUpdate"
37
+ >
38
+ <ToggleGroupItem
39
+ v-for="o in options"
40
+ :key="o.value"
41
+ :value="o.value"
42
+ :disabled="disabled"
43
+ class="ui-btngroup__item"
44
+ >
45
+ <UiIcon v-if="o.icon" :name="o.icon" :size="15" />
46
+ {{ o.label }}
47
+ </ToggleGroupItem>
48
+ </ToggleGroupRoot>
49
+ </template>
50
+
51
+ <style lang="scss">
52
+ @use '../../assets/scss/mixins' as *;
53
+
54
+ .ui-btngroup {
55
+ display: inline-flex;
56
+ // Wrap is required: this is a shared primitive (field/Choice renders it with arbitrary options), so a
57
+ // long set must break onto multiple rows rather than overflow/clip. A 2-item toggle never wraps anyway.
58
+ flex-wrap: wrap;
59
+ border: 1px solid var(--color-control-border, var(--color-border));
60
+ border-radius: var(--radius-md);
61
+ overflow: hidden;
62
+ width: fit-content;
63
+
64
+ &__item {
65
+ @include focus-ring;
66
+ display: inline-flex;
67
+ align-items: center;
68
+ gap: var(--space-1);
69
+ padding: var(--space-2) var(--space-3);
70
+ border: 0;
71
+ border-inline-start: 1px solid var(--color-control-border, var(--color-border));
72
+ background: var(--color-surface);
73
+ color: var(--color-text);
74
+ font-size: var(--text-sm);
75
+ cursor: pointer;
76
+
77
+ &:first-child {
78
+ border-inline-start: 0;
79
+ }
80
+ &:hover {
81
+ background: var(--color-hover);
82
+ }
83
+ // Neutral fill + an indigo label so the active segment reads at a glance (the fill alone is too
84
+ // close to the hover wash to rely on). Fallback for use outside the themed admin shell.
85
+ &[data-state='on'] {
86
+ background: var(--color-active, var(--color-surface-2));
87
+ color: var(--color-primary-on-fill, var(--color-primary));
88
+ font-weight: var(--weight-medium);
89
+ }
90
+ &[data-disabled] {
91
+ opacity: 0.6;
92
+ cursor: not-allowed;
93
+ }
94
+ }
95
+ }
96
+ </style>
@@ -0,0 +1,52 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch, onMounted } from 'vue'
3
+
4
+ const props = withDefaults(
5
+ defineProps<{
6
+ disabled?: boolean
7
+ /** Tri-state marker for a "some but not all" selection (e.g. a select-all header). */
8
+ indeterminate?: boolean
9
+ }>(),
10
+ { disabled: false, indeterminate: false },
11
+ )
12
+
13
+ // Defaulted so the emitted value is a plain `boolean`: an undefined-able model would force every
14
+ // handler to coerce, and an unbound checkbox is unchecked anyway.
15
+ const model = defineModel<boolean>({ default: false })
16
+
17
+ // `indeterminate` is a DOM PROPERTY with no HTML attribute, so it can't be bound in the template and must
18
+ // be assigned imperatively: once on mount (the element now exists) and again whenever the prop changes.
19
+ const input = ref<HTMLInputElement | null>(null)
20
+ function syncIndeterminate() {
21
+ if (input.value) input.value.indeterminate = props.indeterminate
22
+ }
23
+ onMounted(syncIndeterminate)
24
+ watch(() => props.indeterminate, syncIndeterminate)
25
+ </script>
26
+
27
+ <template>
28
+ <input
29
+ ref="input"
30
+ v-model="model"
31
+ type="checkbox"
32
+ :disabled="disabled"
33
+ class="ui-checkbox"
34
+ >
35
+ </template>
36
+
37
+ <style lang="scss">
38
+ @use '../../assets/scss/mixins' as *;
39
+
40
+ .ui-checkbox {
41
+ @include focus-ring;
42
+ width: 1.25rem;
43
+ height: 1.25rem;
44
+ accent-color: var(--color-primary);
45
+ cursor: pointer;
46
+
47
+ &:disabled {
48
+ opacity: 0.6;
49
+ cursor: not-allowed;
50
+ }
51
+ }
52
+ </style>
@@ -0,0 +1,50 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import UiCheckbox from './Checkbox.vue'
4
+
5
+ withDefaults(
6
+ defineProps<{
7
+ options: { label: string; value: string }[]
8
+ disabled?: boolean
9
+ }>(),
10
+ { disabled: false },
11
+ )
12
+
13
+ const model = defineModel<string[]>()
14
+ const selected = computed(() => model.value ?? [])
15
+
16
+ function toggle(value: string, checked: boolean) {
17
+ const set = new Set(selected.value)
18
+ if (checked) set.add(value)
19
+ else set.delete(value)
20
+ model.value = [...set]
21
+ }
22
+ </script>
23
+
24
+ <template>
25
+ <div class="ui-checkbox-group" role="group">
26
+ <label v-for="o in options" :key="o.value" class="ui-checkbox-group__item">
27
+ <UiCheckbox
28
+ :model-value="selected.includes(o.value)"
29
+ :disabled="disabled"
30
+ @update:model-value="(c) => toggle(o.value, c)"
31
+ />
32
+ <span>{{ o.label }}</span>
33
+ </label>
34
+ </div>
35
+ </template>
36
+
37
+ <style lang="scss">
38
+ .ui-checkbox-group {
39
+ display: flex;
40
+ flex-direction: column;
41
+ gap: var(--space-2);
42
+
43
+ &__item {
44
+ display: flex;
45
+ align-items: center;
46
+ gap: var(--space-2);
47
+ cursor: pointer;
48
+ }
49
+ }
50
+ </style>
@@ -0,0 +1,343 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch, nextTick } from 'vue'
3
+ import UiIcon from './Icon.vue'
4
+ import {
5
+ ComboboxRoot, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxCancel,
6
+ ComboboxContent, ComboboxViewport, ComboboxItem, ComboboxItemIndicator,
7
+ } from 'reka-ui'
8
+ import type { FieldOption } from '../../utils/field-component'
9
+ import { reorder } from '../../utils/reorder'
10
+
11
+ const props = withDefaults(
12
+ defineProps<{
13
+ options: FieldOption[]
14
+ selected: FieldOption[]
15
+ multiple?: boolean
16
+ loading?: boolean
17
+ placeholder?: string
18
+ disabled?: boolean
19
+ inputId?: string
20
+ invalid?: boolean
21
+ describedby?: string
22
+ required?: boolean
23
+ }>(),
24
+ { multiple: false, loading: false, disabled: false, invalid: false, required: false },
25
+ )
26
+
27
+ const model = defineModel<number | number[] | null>()
28
+ const emit = defineEmits<{ search: [term: string] }>()
29
+
30
+ const query = ref('')
31
+
32
+ // Reka's root model: an array for multiple, the value (or undefined) for single.
33
+ const rootModel = computed<number | number[] | undefined>({
34
+ get: () => (props.multiple
35
+ ? (Array.isArray(model.value) ? model.value : [])
36
+ : (typeof model.value === 'number' ? model.value : undefined)),
37
+ set: (v) => { model.value = props.multiple ? ((v as number[]) ?? []) : (typeof v === 'number' ? v : null) },
38
+ })
39
+
40
+ function labelFor(id: number): string {
41
+ return props.selected.find((s) => s.value === id)?.label
42
+ ?? props.options.find((o) => o.value === id)?.label
43
+ ?? `#${id}`
44
+ }
45
+ const displayValue = (id: unknown) => (typeof id === 'number' ? labelFor(id) : '')
46
+
47
+ // In single mode Reka resyncs the input text to the selected label on select/blur;
48
+ // that programmatic update must not be treated as a user search.
49
+ const currentLabel = computed(() => (!props.multiple && typeof model.value === 'number' ? labelFor(model.value) : null))
50
+
51
+ // A selected label can resolve asynchronously (id known, label fetched later) with no model change,
52
+ // which Reka's display-value callback won't re-evaluate — mirror it into the single-mode input.
53
+ watch(currentLabel, (lbl) => { if (lbl != null) query.value = lbl })
54
+
55
+ function onInput(term: string) {
56
+ query.value = term
57
+ if (term === currentLabel.value) return
58
+ emit('search', term)
59
+ }
60
+ // Focusing an empty box loads the first page of records, so options appear without typing.
61
+ function onFocus() {
62
+ if (!query.value.trim()) emit('search', '')
63
+ }
64
+ function removeChip(id: number) {
65
+ if (Array.isArray(model.value)) model.value = model.value.filter((v) => v !== id)
66
+ }
67
+
68
+ const { t } = useT()
69
+
70
+ const chips = ref<HTMLElement | null>(null)
71
+ const liveMessage = ref('')
72
+
73
+ function announceMove(label: string, to: number) {
74
+ liveMessage.value = t('combobox.moved', { label, pos: to + 1, total: props.selected.length })
75
+ }
76
+
77
+ // The chip index IS the model index here, so commit reorders the model directly.
78
+ const { dragIndex, overIndex, onDragStart, onDragEnter, onDragLeave, onDrop, onDragEnd } = useDragReorder({
79
+ disabled: () => props.disabled || !Array.isArray(model.value),
80
+ commit: (from, to) => {
81
+ // Re-stated here rather than relying on the `disabled` predicate above: only the local check narrows
82
+ // the model to the multi-value array `reorder` returns (and that `model.value` accepts back).
83
+ if (!Array.isArray(model.value)) return
84
+ const label = props.selected[from]?.label ?? ''
85
+ model.value = reorder(model.value, from, to)
86
+ nextTick(() => announceMove(label, to))
87
+ },
88
+ })
89
+
90
+ function moveChip(from: number, to: number) {
91
+ if (props.disabled || !Array.isArray(model.value) || to < 0 || to >= model.value.length) return
92
+ const dir = to < from ? 'earlier' : 'later'
93
+ const label = props.selected[from]?.label ?? ''
94
+ model.value = reorder(model.value, from, to)
95
+ // Keep keyboard focus on the moved chip: keyed reuse preserves it for the pressed button, but when that
96
+ // button becomes disabled at a boundary the browser drops focus to <body> — redirect to the remove button.
97
+ nextTick(() => {
98
+ announceMove(label, to)
99
+ const chip = chips.value?.children[to] as HTMLElement | undefined
100
+ if (!chip) return
101
+ const moveBtns = chip.querySelectorAll<HTMLButtonElement>('.ui-combobox__chip-move')
102
+ const dirBtn = dir === 'earlier' ? moveBtns[0] : moveBtns[1]
103
+ ;(dirBtn && !dirBtn.disabled ? dirBtn : chip.querySelector<HTMLButtonElement>('.ui-combobox__chip-remove'))?.focus()
104
+ })
105
+ }
106
+ </script>
107
+
108
+ <template>
109
+ <ComboboxRoot
110
+ v-model="rootModel"
111
+ :multiple="multiple"
112
+ :disabled="disabled"
113
+ ignore-filter
114
+ reset-model-value-on-clear
115
+ :open-on-click="true"
116
+ :open-on-focus="true"
117
+ class="ui-combobox"
118
+ >
119
+ <ComboboxAnchor class="ui-combobox__anchor" :data-multiple="multiple || undefined" :aria-invalid="invalid || undefined">
120
+ <ul v-if="multiple && selected.length" ref="chips" class="ui-combobox__chips" role="list" @dragleave="onDragLeave">
121
+ <li
122
+ v-for="(s, i) in selected"
123
+ :key="s.value"
124
+ class="ui-combobox__chip"
125
+ :class="{ 'ui-combobox__chip--over': overIndex === i && dragIndex !== i }"
126
+ :draggable="!disabled"
127
+ @dragstart="onDragStart(i, $event)"
128
+ @dragenter="onDragEnter(i)"
129
+ @dragover.prevent="onDragEnter(i)"
130
+ @drop.prevent="onDrop(i)"
131
+ @dragend="onDragEnd"
132
+ >
133
+ <button
134
+ type="button"
135
+ draggable="false"
136
+ class="ui-combobox__chip-move"
137
+ :aria-label="t('combobox.move_earlier', { label: s.label })"
138
+ :disabled="disabled || i === 0"
139
+ @click="moveChip(i, i - 1)"
140
+ ><UiIcon name="chevron-left" :size="14" /></button>
141
+ <span>{{ s.label }}</span>
142
+ <button
143
+ type="button"
144
+ draggable="false"
145
+ class="ui-combobox__chip-move"
146
+ :aria-label="t('combobox.move_later', { label: s.label })"
147
+ :disabled="disabled || i === selected.length - 1"
148
+ @click="moveChip(i, i + 1)"
149
+ ><UiIcon name="chevron-right" :size="14" /></button>
150
+ <button
151
+ type="button"
152
+ draggable="false"
153
+ class="ui-combobox__chip-remove"
154
+ :aria-label="t('combobox.remove', { label: s.label })"
155
+ :disabled="disabled"
156
+ @click="removeChip(s.value)"
157
+ ><UiIcon name="x" :size="14" /></button>
158
+ </li>
159
+ </ul>
160
+ <ComboboxInput
161
+ :id="inputId"
162
+ class="ui-combobox__input"
163
+ :model-value="query"
164
+ :placeholder="placeholder"
165
+ :display-value="multiple ? undefined : displayValue"
166
+ :disabled="disabled"
167
+ :aria-invalid="invalid || undefined"
168
+ :aria-describedby="describedby"
169
+ :required="required || undefined"
170
+ @update:model-value="onInput"
171
+ @focus="onFocus"
172
+ />
173
+ <ComboboxCancel v-if="!multiple" class="ui-combobox__icon" :aria-label="t('combobox.clear')"><UiIcon name="x" :size="14" /></ComboboxCancel>
174
+ <ComboboxTrigger class="ui-combobox__icon" :aria-label="t('combobox.toggle_options')"><UiIcon name="chevron-down" :size="14" /></ComboboxTrigger>
175
+ </ComboboxAnchor>
176
+
177
+ <ComboboxContent class="ui-combobox__content" :side-offset="4">
178
+ <ComboboxViewport class="ui-combobox__viewport">
179
+ <p v-if="loading" class="ui-combobox__note">{{ t('combobox.loading') }}</p>
180
+ <p v-else-if="!options.length" class="ui-combobox__note">{{ t('combobox.no_results') }}</p>
181
+ <ComboboxItem
182
+ v-for="o in options"
183
+ :key="o.value"
184
+ :value="o.value"
185
+ class="ui-combobox__item"
186
+ >
187
+ <span>{{ o.label }}</span>
188
+ <ComboboxItemIndicator class="ui-combobox__check"><UiIcon name="check" :size="14" /></ComboboxItemIndicator>
189
+ </ComboboxItem>
190
+ </ComboboxViewport>
191
+ </ComboboxContent>
192
+
193
+ <span class="ui-combobox__live" aria-live="polite">{{ liveMessage }}</span>
194
+ </ComboboxRoot>
195
+ </template>
196
+
197
+ <style lang="scss">
198
+ @use '../../assets/scss/mixins' as *;
199
+
200
+ .ui-combobox {
201
+ &__anchor {
202
+ display: flex;
203
+ flex-wrap: wrap;
204
+ align-items: center;
205
+ gap: var(--space-2);
206
+ padding: var(--space-1) var(--space-2);
207
+ border: 1px solid var(--color-control-border, var(--color-border));
208
+ border-radius: var(--radius-md);
209
+ background: var(--color-surface);
210
+
211
+ &:focus-within { outline: 2px solid var(--color-focus); outline-offset: -2px; }
212
+ &[aria-invalid='true'] { border-color: var(--color-danger); }
213
+ }
214
+ &__chips {
215
+ display: flex;
216
+ flex-wrap: wrap;
217
+ gap: var(--space-1);
218
+ list-style: none;
219
+ margin: 0;
220
+ padding: 0;
221
+ }
222
+ &__chip {
223
+ display: inline-flex;
224
+ align-items: center;
225
+ gap: var(--space-1);
226
+ padding: 0 var(--space-1) 0 var(--space-2);
227
+ background: var(--color-bg);
228
+ border: 1px solid var(--color-border);
229
+ border-radius: var(--radius-sm);
230
+ font-size: var(--text-sm);
231
+ }
232
+ &__chip-remove {
233
+ @include focus-ring;
234
+ display: inline-flex;
235
+ align-items: center;
236
+ justify-content: center;
237
+ height: 1.5rem;
238
+ padding: 0;
239
+ border: 0;
240
+ background: transparent;
241
+ cursor: pointer;
242
+ color: var(--color-text-muted);
243
+ font-size: var(--text-base);
244
+ line-height: 1;
245
+ }
246
+ &__chip-move {
247
+ @include focus-ring;
248
+ display: inline-flex;
249
+ align-items: center;
250
+ justify-content: center;
251
+ height: 1.5rem;
252
+ border: 0;
253
+ background: transparent;
254
+ cursor: pointer;
255
+ color: var(--color-text-muted);
256
+ font-size: var(--text-base);
257
+ line-height: 1;
258
+ // Collapsed + transparent when idle so the chip stays compact, but NOT display/visibility:none —
259
+ // the button must remain focusable so keyboard users can Tab to it (which then reveals it via
260
+ // :focus-within below). Revealed on hover/focus, and always shown where there is no hover (touch).
261
+ opacity: 0;
262
+ max-width: 0;
263
+ padding: 0;
264
+ overflow: hidden;
265
+ // animate only size, not opacity, so the focus ring shows immediately on keyboard focus
266
+ transition: max-width 0.1s, padding 0.1s;
267
+
268
+ &:disabled { cursor: default; } // stays collapsed (the reveal selector excludes :disabled)
269
+ }
270
+ &__live { @include sr-only; }
271
+ &__chip {
272
+ &:hover,
273
+ &:focus-within {
274
+ .ui-combobox__chip-move:not(:disabled) {
275
+ opacity: 1;
276
+ max-width: 1.5rem;
277
+ padding: 0 var(--space-1);
278
+ }
279
+ }
280
+ }
281
+ @media (hover: none) {
282
+ &__chip-move:not(:disabled) {
283
+ opacity: 1;
284
+ max-width: 1.5rem;
285
+ padding: 0 var(--space-1);
286
+ }
287
+ }
288
+ &__chip--over {
289
+ outline: 2px solid var(--color-primary);
290
+ outline-offset: -2px;
291
+ }
292
+ &__input {
293
+ flex: 1;
294
+ min-width: 6rem;
295
+ border: 0;
296
+ outline: none;
297
+ background: transparent;
298
+ color: var(--color-text);
299
+ font-size: var(--text-base);
300
+ padding: var(--space-1) 0;
301
+ }
302
+ &__icon {
303
+ @include focus-ring;
304
+ border: 0;
305
+ background: transparent;
306
+ cursor: pointer;
307
+ color: var(--color-text-muted);
308
+ }
309
+ &__content {
310
+ z-index: var(--z-dropdown);
311
+ width: var(--reka-combobox-trigger-width);
312
+ max-height: 16rem;
313
+ overflow: hidden;
314
+ background: var(--color-surface);
315
+ border: 1px solid var(--color-border);
316
+ border-radius: var(--radius-md);
317
+ box-shadow: var(--shadow-md);
318
+ }
319
+ &__viewport {
320
+ max-height: 16rem;
321
+ overflow-y: auto;
322
+ padding: var(--space-1);
323
+ }
324
+ &__note {
325
+ padding: var(--space-2) var(--space-3);
326
+ color: var(--color-text-muted);
327
+ font-size: var(--text-sm);
328
+ }
329
+ &__item {
330
+ display: flex;
331
+ align-items: center;
332
+ justify-content: space-between;
333
+ gap: var(--space-2);
334
+ padding: var(--space-2) var(--space-3);
335
+ border-radius: var(--radius-sm);
336
+ cursor: pointer;
337
+
338
+ &[data-highlighted] { background: var(--color-hover); outline: none; }
339
+ &[data-state='checked'] { font-weight: var(--weight-medium); }
340
+ }
341
+ &__check { color: var(--color-primary); }
342
+ }
343
+ </style>