@michaelthielemann/kestrel 4.1.3 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (340) hide show
  1. package/README.md +26 -201
  2. package/dist/boot.d.ts +45 -0
  3. package/dist/boot.js +274 -0
  4. package/dist/cast.d.ts +3 -0
  5. package/dist/cast.js +6 -0
  6. package/dist/catalogue.d.ts +11 -0
  7. package/dist/catalogue.js +1 -0
  8. package/dist/cli.d.ts +2 -0
  9. package/dist/cli.js +55 -0
  10. package/dist/context.d.ts +62 -0
  11. package/dist/context.js +61 -0
  12. package/dist/dataflow.d.ts +3 -0
  13. package/dist/dataflow.js +65 -0
  14. package/dist/defineConfig.d.ts +157 -0
  15. package/dist/defineConfig.js +41 -0
  16. package/dist/defineContract.d.ts +12 -0
  17. package/dist/defineContract.js +16 -0
  18. package/dist/defineModule.d.ts +75 -0
  19. package/dist/defineModule.js +9 -0
  20. package/dist/definePipeline.d.ts +7 -0
  21. package/dist/definePipeline.js +11 -0
  22. package/dist/errors.d.ts +26 -0
  23. package/dist/errors.js +48 -0
  24. package/dist/index.d.ts +19 -0
  25. package/dist/index.js +14 -0
  26. package/dist/load.d.ts +7 -0
  27. package/dist/load.js +79 -0
  28. package/dist/logger.d.ts +18 -0
  29. package/dist/logger.js +36 -0
  30. package/dist/registry.d.ts +14 -0
  31. package/dist/registry.js +87 -0
  32. package/dist/result.d.ts +19 -0
  33. package/dist/result.js +19 -0
  34. package/dist/runner.d.ts +29 -0
  35. package/dist/runner.js +133 -0
  36. package/dist/schema.d.ts +7 -0
  37. package/dist/schema.js +142 -0
  38. package/dist/sort.d.ts +2 -0
  39. package/dist/sort.js +50 -0
  40. package/dist/testing/runPipeline.d.ts +9 -0
  41. package/dist/testing/runPipeline.js +22 -0
  42. package/dist/triggers/cron.d.ts +10 -0
  43. package/dist/triggers/cron.js +44 -0
  44. package/dist/triggers/cronMatch.d.ts +10 -0
  45. package/dist/triggers/cronMatch.js +68 -0
  46. package/dist/triggers/http.d.ts +58 -0
  47. package/dist/triggers/http.js +292 -0
  48. package/package.json +25 -151
  49. package/NOTICE +0 -12
  50. package/kestrel.config.ts +0 -28
  51. package/layers/access/nuxt.config.ts +0 -5
  52. package/layers/access/server/middleware/00.ip-allowlist.ts +0 -52
  53. package/layers/access/server/middleware/access-guard.ts +0 -32
  54. package/layers/access/server/types.d.ts +0 -7
  55. package/layers/admin/app/components/AdminAccount.vue +0 -212
  56. package/layers/admin/app/components/AdminNav.vue +0 -44
  57. package/layers/admin/app/components/BlockFields.vue +0 -67
  58. package/layers/admin/app/components/BlockPreview.vue +0 -413
  59. package/layers/admin/app/components/BlockTree.vue +0 -433
  60. package/layers/admin/app/components/BlocksBody.vue +0 -163
  61. package/layers/admin/app/components/CollectionDeleteDialog.vue +0 -38
  62. package/layers/admin/app/components/CollectionEditor.vue +0 -324
  63. package/layers/admin/app/components/CollectionList.vue +0 -429
  64. package/layers/admin/app/components/CollectionListBulkBar.vue +0 -62
  65. package/layers/admin/app/components/CollectionListColumnsPanel.vue +0 -37
  66. package/layers/admin/app/components/CollectionListFilterPanel.vue +0 -145
  67. package/layers/admin/app/components/CollectionListPager.vue +0 -70
  68. package/layers/admin/app/components/CollectionListTable.vue +0 -347
  69. package/layers/admin/app/components/EditorStatus.vue +0 -211
  70. package/layers/admin/app/components/EditorUnsupported.vue +0 -28
  71. package/layers/admin/app/components/FieldsBody.vue +0 -23
  72. package/layers/admin/app/components/LocaleBar.vue +0 -107
  73. package/layers/admin/app/components/PageFields.vue +0 -129
  74. package/layers/admin/app/components/PageFieldsPane.vue +0 -39
  75. package/layers/admin/app/components/SeoFields.vue +0 -210
  76. package/layers/admin/app/components/SingletonEditor.vue +0 -93
  77. package/layers/admin/app/composables/useAuth.ts +0 -68
  78. package/layers/admin/app/composables/useBlockTree.ts +0 -95
  79. package/layers/admin/app/composables/useBlocks.ts +0 -7
  80. package/layers/admin/app/composables/useCachedResource.ts +0 -35
  81. package/layers/admin/app/composables/useCollectionOps.ts +0 -102
  82. package/layers/admin/app/composables/useCollections.ts +0 -6
  83. package/layers/admin/app/composables/useContentLocales.ts +0 -23
  84. package/layers/admin/app/composables/useEditForm.ts +0 -402
  85. package/layers/admin/app/composables/useLinkResolver.ts +0 -40
  86. package/layers/admin/app/composables/useListBatchActions.ts +0 -78
  87. package/layers/admin/app/composables/useListColumns.ts +0 -53
  88. package/layers/admin/app/composables/useListFilterDraft.ts +0 -129
  89. package/layers/admin/app/composables/useListRows.ts +0 -75
  90. package/layers/admin/app/composables/useListSelection.ts +0 -28
  91. package/layers/admin/app/composables/useListUrlState.ts +0 -53
  92. package/layers/admin/app/composables/usePendingPublishPoll.ts +0 -15
  93. package/layers/admin/app/composables/usePreviewViewport.ts +0 -30
  94. package/layers/admin/app/composables/usePublishStatus.ts +0 -107
  95. package/layers/admin/app/composables/useRailCollapsed.ts +0 -14
  96. package/layers/admin/app/composables/useTheme.ts +0 -36
  97. package/layers/admin/app/composables/useToolbarPanel.ts +0 -38
  98. package/layers/admin/app/composables/useUnsavedGuard.ts +0 -22
  99. package/layers/admin/app/layouts/admin.vue +0 -266
  100. package/layers/admin/app/middleware/admin-auth.ts +0 -4
  101. package/layers/admin/app/pages/admin/[collection]/[id].vue +0 -203
  102. package/layers/admin/app/pages/admin/[collection]/index.vue +0 -62
  103. package/layers/admin/app/pages/admin/index.vue +0 -118
  104. package/layers/admin/app/pages/admin/login.vue +0 -68
  105. package/layers/admin/app/pages/admin/media.vue +0 -28
  106. package/layers/admin/app/pages/admin/references.vue +0 -146
  107. package/layers/admin/app/plugins/admin-style-guard.client.ts +0 -76
  108. package/layers/admin/app/plugins/reauth.client.ts +0 -20
  109. package/layers/admin/app/utils/admin-style-guard.ts +0 -20
  110. package/layers/admin/app/utils/block-tree.ts +0 -214
  111. package/layers/admin/app/utils/collection-ops.ts +0 -69
  112. package/layers/admin/app/utils/dead-refs.ts +0 -30
  113. package/layers/admin/app/utils/edit-form.ts +0 -267
  114. package/layers/admin/app/utils/editor-expose.ts +0 -36
  115. package/layers/admin/app/utils/editor-form-context.ts +0 -82
  116. package/layers/admin/app/utils/editor-registry.ts +0 -20
  117. package/layers/admin/app/utils/field-keys.ts +0 -11
  118. package/layers/admin/app/utils/list-cell.ts +0 -46
  119. package/layers/admin/app/utils/list-columns.ts +0 -142
  120. package/layers/admin/app/utils/list-query.ts +0 -96
  121. package/layers/admin/app/utils/nav-active.ts +0 -9
  122. package/layers/admin/app/utils/populate-blocks.ts +0 -288
  123. package/layers/admin/app/utils/preview-channel.ts +0 -62
  124. package/layers/admin/app/utils/preview-viewport.ts +0 -80
  125. package/layers/admin/app/utils/reauth.ts +0 -67
  126. package/layers/admin/app/utils/record-title.ts +0 -15
  127. package/layers/admin/app/utils/register-builtin-editors.ts +0 -9
  128. package/layers/admin/app/utils/safe-redirect.ts +0 -12
  129. package/layers/admin/nuxt.config.ts +0 -20
  130. package/layers/auth/nuxt.config.ts +0 -1
  131. package/layers/auth/server/plugins/01.register-auth-pipelines.ts +0 -7
  132. package/layers/collections/nuxt.config.ts +0 -1
  133. package/layers/collections/server/plugins/01.register.ts +0 -29
  134. package/layers/collections/server/plugins/02.register-relation-populate.ts +0 -18
  135. package/layers/core/app/composables/layouts.ts +0 -5
  136. package/layers/core/app/utils/field-factories.ts +0 -129
  137. package/layers/core/app/utils/layouts.ts +0 -38
  138. package/layers/core/modules/auto-discovery/extract-block.ts +0 -179
  139. package/layers/core/modules/auto-discovery/index.ts +0 -112
  140. package/layers/core/modules/auto-discovery/package-registry.ts +0 -19
  141. package/layers/core/modules/auto-discovery/scan.ts +0 -170
  142. package/layers/core/modules/auto-discovery/virtual.d.ts +0 -20
  143. package/layers/core/modules/component-namespace/index.ts +0 -44
  144. package/layers/core/modules/component-namespace/shared.ts +0 -24
  145. package/layers/core/modules/kestrel/app-shell.ts +0 -55
  146. package/layers/core/modules/kestrel/auto-imports.ts +0 -44
  147. package/layers/core/modules/kestrel/index.ts +0 -144
  148. package/layers/core/modules/plugin-order/index.ts +0 -69
  149. package/layers/core/modules/plugin-order/plugin-order.ts +0 -273
  150. package/layers/core/nuxt.config.ts +0 -10
  151. package/layers/core/server/api/[...path].ts +0 -98
  152. package/layers/core/server/plugins/00.config.ts +0 -12
  153. package/layers/core/server/plugins/00.migrate.ts +0 -12
  154. package/layers/core/server/plugins/01.register-introspection-pipeline.ts +0 -5
  155. package/layers/core/server/plugins/01.register-openapi-pipeline.ts +0 -5
  156. package/layers/core/server/plugins/01.register-tooling-pipelines.ts +0 -5
  157. package/layers/core/server/plugins/02.schema-sync.ts +0 -58
  158. package/layers/core/server/plugins/04.outbox-worker.ts +0 -31
  159. package/layers/core/server/plugins/05.reindex-refs.ts +0 -5
  160. package/layers/core/server/routes/__kestrel/dashboard.get.ts +0 -29
  161. package/layers/core/server/tasks/db/migrate-module.ts +0 -83
  162. package/layers/core/server/tasks/db/migrate-revisions.ts +0 -20
  163. package/layers/core/server/tasks/db/migrate.ts +0 -50
  164. package/layers/core/server/utils/server-config.ts +0 -81
  165. package/layers/fields/nuxt.config.ts +0 -1
  166. package/layers/fields/server/plugins/01.register-blocks-pipeline.ts +0 -7
  167. package/layers/fields/server/plugins/01.register-field-populate.ts +0 -11
  168. package/layers/media/app/components/KestrelImg.vue +0 -94
  169. package/layers/media/app/components/MediaConflictDialog.vue +0 -35
  170. package/layers/media/app/components/MediaDeleteDialog.vue +0 -52
  171. package/layers/media/app/components/MediaGrid.vue +0 -113
  172. package/layers/media/app/components/MediaImage.vue +0 -70
  173. package/layers/media/app/components/MediaLibrary.vue +0 -456
  174. package/layers/media/app/components/MediaNewFolderDialog.vue +0 -32
  175. package/layers/media/app/components/MediaPathBar.vue +0 -163
  176. package/layers/media/app/components/MediaPicker.vue +0 -19
  177. package/layers/media/app/components/MediaRenameDialog.vue +0 -32
  178. package/layers/media/app/components/MediaTable.vue +0 -75
  179. package/layers/media/app/components/MediaThumb.vue +0 -25
  180. package/layers/media/app/components/MediaToolbar.vue +0 -90
  181. package/layers/media/app/components/MediaUploadDialog.vue +0 -87
  182. package/layers/media/app/components/MediaViewer.vue +0 -137
  183. package/layers/media/app/components/field/Media.vue +0 -263
  184. package/layers/media/app/composables/useBlurUp.ts +0 -55
  185. package/layers/media/app/composables/useMediaClipboard.ts +0 -14
  186. package/layers/media/app/composables/useMediaContextMenu.ts +0 -56
  187. package/layers/media/app/composables/useMediaDnd.ts +0 -56
  188. package/layers/media/app/composables/useMediaLibrary.ts +0 -152
  189. package/layers/media/app/composables/useMediaOps.ts +0 -50
  190. package/layers/media/app/composables/useMediaRelocate.ts +0 -39
  191. package/layers/media/app/composables/useMediaResolver.ts +0 -50
  192. package/layers/media/app/composables/useMediaUpload.ts +0 -93
  193. package/layers/media/app/composables/useMediaVariant.ts +0 -34
  194. package/layers/media/app/plugins/field-media.client.ts +0 -6
  195. package/layers/media/app/utils/ai-disclosure.ts +0 -19
  196. package/layers/media/app/utils/dnd.ts +0 -69
  197. package/layers/media/app/utils/library.ts +0 -81
  198. package/layers/media/app/utils/ops.ts +0 -60
  199. package/layers/media/app/utils/picture-model.ts +0 -125
  200. package/layers/media/app/utils/thumbhash.ts +0 -19
  201. package/layers/media/app/utils/variant-name.ts +0 -20
  202. package/layers/media/nuxt.config.ts +0 -7
  203. package/layers/media/server/middleware/ondemand-variants.ts +0 -38
  204. package/layers/media/server/plugins/01.register-media-pipelines.ts +0 -7
  205. package/layers/media/server/plugins/02.register-media.ts +0 -22
  206. package/layers/media/server/plugins/04.variant-capture.ts +0 -27
  207. package/layers/media/server/plugins/05.media-cleanup.ts +0 -5
  208. package/layers/media/server/tasks/media/backfill.ts +0 -17
  209. package/layers/public/app/app.vue +0 -5
  210. package/layers/public/app/components/BlockRenderer.vue +0 -56
  211. package/layers/public/app/components/KestrelLink.vue +0 -26
  212. package/layers/public/app/components/KestrelPreviewBridge.vue +0 -133
  213. package/layers/public/app/composables/public-page.ts +0 -24
  214. package/layers/public/app/error.vue +0 -12
  215. package/layers/public/app/layouts/default.vue +0 -5
  216. package/layers/public/app/pages/[...slug].vue +0 -239
  217. package/layers/public/app/pages/__kestrel/preview.vue +0 -46
  218. package/layers/public/app/utils/block-edit-context.ts +0 -16
  219. package/layers/public/app/utils/json-ld.ts +0 -139
  220. package/layers/public/app/utils/link-href.ts +0 -46
  221. package/layers/public/app/utils/page-head.ts +0 -84
  222. package/layers/public/app/utils/page-layout.ts +0 -18
  223. package/layers/public/app/utils/preview-protocol.ts +0 -117
  224. package/layers/public/app/utils/public-route.ts +0 -46
  225. package/layers/public/app/utils/site-head.ts +0 -43
  226. package/layers/public/modules/deploy-output/index.ts +0 -80
  227. package/layers/public/modules/prerender-routes/discover.ts +0 -37
  228. package/layers/public/modules/prerender-routes/index.ts +0 -88
  229. package/layers/public/modules/prerender-routes/routes.ts +0 -16
  230. package/layers/public/modules/prune-media/index.ts +0 -48
  231. package/layers/public/modules/prune-media/prune-media.ts +0 -81
  232. package/layers/public/nuxt.config.ts +0 -14
  233. package/layers/public/server/delivery-live/serve.ts +0 -111
  234. package/layers/public/server/middleware/delivery-live-catchall.ts +0 -7
  235. package/layers/public/server/pipelines/index.ts +0 -23
  236. package/layers/public/server/plugins/00.ensure-snapshot-triggers.ts +0 -14
  237. package/layers/public/server/plugins/01.register-public-pipelines.ts +0 -7
  238. package/layers/public/server/plugins/02.register-links.ts +0 -16
  239. package/layers/public/server/plugins/03.redirects.ts +0 -55
  240. package/layers/public/server/plugins/05.plan-publish.ts +0 -5
  241. package/layers/public/server/plugins/zz.publish.ts +0 -127
  242. package/layers/public/server/routes/llms-full.txt.get.ts +0 -99
  243. package/layers/public/server/routes/llms.txt.get.ts +0 -66
  244. package/layers/public/server/routes/redirects.json.get.ts +0 -58
  245. package/layers/public/server/routes/robots.txt.get.ts +0 -11
  246. package/layers/public/server/routes/sitemap.xml.get.ts +0 -79
  247. package/layers/public/server/tasks/publish/run.ts +0 -32
  248. package/layers/public/server/utils/publish/render-live.ts +0 -26
  249. package/layers/ui/app/assets/scss/_base.scss +0 -16
  250. package/layers/ui/app/assets/scss/_datepicker.scss +0 -82
  251. package/layers/ui/app/assets/scss/_mixins.scss +0 -50
  252. package/layers/ui/app/assets/scss/_reset.scss +0 -65
  253. package/layers/ui/app/assets/scss/_tokens.scss +0 -183
  254. package/layers/ui/app/assets/scss/main.scss +0 -4
  255. package/layers/ui/app/components/field/Boolean.vue +0 -30
  256. package/layers/ui/app/components/field/Choice.vue +0 -73
  257. package/layers/ui/app/components/field/Datetime.vue +0 -98
  258. package/layers/ui/app/components/field/DeadRefNote.vue +0 -28
  259. package/layers/ui/app/components/field/Json.vue +0 -62
  260. package/layers/ui/app/components/field/Layout.vue +0 -138
  261. package/layers/ui/app/components/field/Link.vue +0 -112
  262. package/layers/ui/app/components/field/LinkInternalPicker.vue +0 -95
  263. package/layers/ui/app/components/field/Number.vue +0 -30
  264. package/layers/ui/app/components/field/Relation.vue +0 -49
  265. package/layers/ui/app/components/field/Renderer.vue +0 -30
  266. package/layers/ui/app/components/field/Repeater.vue +0 -460
  267. package/layers/ui/app/components/field/Richtext.vue +0 -26
  268. package/layers/ui/app/components/field/Slug.vue +0 -65
  269. package/layers/ui/app/components/field/Text.vue +0 -35
  270. package/layers/ui/app/components/field/Unsupported.vue +0 -34
  271. package/layers/ui/app/components/ui/Alert.vue +0 -50
  272. package/layers/ui/app/components/ui/Brand.vue +0 -22
  273. package/layers/ui/app/components/ui/Button.vue +0 -116
  274. package/layers/ui/app/components/ui/ButtonGroup.vue +0 -96
  275. package/layers/ui/app/components/ui/Checkbox.vue +0 -53
  276. package/layers/ui/app/components/ui/CheckboxGroup.vue +0 -51
  277. package/layers/ui/app/components/ui/Combobox.vue +0 -345
  278. package/layers/ui/app/components/ui/DatePicker.vue +0 -92
  279. package/layers/ui/app/components/ui/DateRangePicker.vue +0 -100
  280. package/layers/ui/app/components/ui/Dialog.vue +0 -107
  281. package/layers/ui/app/components/ui/EmptyState.vue +0 -46
  282. package/layers/ui/app/components/ui/Field.vue +0 -58
  283. package/layers/ui/app/components/ui/Fieldset.vue +0 -65
  284. package/layers/ui/app/components/ui/Icon.vue +0 -44
  285. package/layers/ui/app/components/ui/Menu.vue +0 -54
  286. package/layers/ui/app/components/ui/NumberInput.vue +0 -175
  287. package/layers/ui/app/components/ui/Richtext.vue +0 -141
  288. package/layers/ui/app/components/ui/RichtextToolbar.vue +0 -251
  289. package/layers/ui/app/components/ui/Select.vue +0 -52
  290. package/layers/ui/app/components/ui/TextInput.vue +0 -111
  291. package/layers/ui/app/components/ui/Textarea.vue +0 -39
  292. package/layers/ui/app/components/ui/TimeInput.vue +0 -18
  293. package/layers/ui/app/components/ui/Toasts.vue +0 -103
  294. package/layers/ui/app/components/ui/Tooltip.vue +0 -45
  295. package/layers/ui/app/composables/useAdminLang.ts +0 -8
  296. package/layers/ui/app/composables/useDragReorder.ts +0 -41
  297. package/layers/ui/app/composables/useEchoGuard.ts +0 -28
  298. package/layers/ui/app/composables/useLinkField.ts +0 -70
  299. package/layers/ui/app/composables/useRecordOptions.ts +0 -84
  300. package/layers/ui/app/composables/useRepeater.ts +0 -99
  301. package/layers/ui/app/composables/useT.ts +0 -32
  302. package/layers/ui/app/composables/useToast.ts +0 -46
  303. package/layers/ui/app/i18n/de.ts +0 -432
  304. package/layers/ui/app/i18n/en.ts +0 -434
  305. package/layers/ui/app/utils/boolean-field.ts +0 -10
  306. package/layers/ui/app/utils/field-component.ts +0 -22
  307. package/layers/ui/app/utils/field-empty.ts +0 -21
  308. package/layers/ui/app/utils/field-registry.ts +0 -36
  309. package/layers/ui/app/utils/humanize.ts +0 -14
  310. package/layers/ui/app/utils/icons.ts +0 -143
  311. package/layers/ui/app/utils/iso-date.ts +0 -18
  312. package/layers/ui/app/utils/localized.ts +0 -13
  313. package/layers/ui/app/utils/record-context.ts +0 -15
  314. package/layers/ui/app/utils/reorder.ts +0 -8
  315. package/layers/ui/app/utils/richtext-preserve-class.ts +0 -31
  316. package/layers/ui/app/utils/useFieldA11y.ts +0 -40
  317. package/layers/ui/nuxt.config.ts +0 -9
  318. package/nuxt.config.ts +0 -108
  319. package/scripts/consumer-template-ci.mjs +0 -160
  320. package/scripts/copy-create-payload.mjs +0 -49
  321. package/scripts/coverage-all.mjs +0 -163
  322. package/scripts/dashboard.mjs +0 -89
  323. package/scripts/hash-password.mjs +0 -29
  324. package/scripts/kestrel.mjs +0 -240
  325. package/scripts/lib/cli.mjs +0 -129
  326. package/scripts/lib/coverage-model.mjs +0 -51
  327. package/scripts/lib/password.mjs +0 -19
  328. package/scripts/lib/scaffold.mjs +0 -205
  329. package/scripts/port-graph.mjs +0 -350
  330. package/scripts/typecheck.mjs +0 -62
  331. package/templates/starter/README.md +0 -65
  332. package/templates/starter/_env.example +0 -17
  333. package/templates/starter/_gitignore +0 -26
  334. package/templates/starter/_package.json +0 -22
  335. package/templates/starter/app/app.vue +0 -7
  336. package/templates/starter/app/blocks/Prose.vue +0 -13
  337. package/templates/starter/app/layouts/default.vue +0 -6
  338. package/templates/starter/nuxt.config.ts +0 -23
  339. package/templates/starter/pnpm-workspace.yaml +0 -8
  340. package/templates/starter/tsconfig.json +0 -3
package/package.json CHANGED
@@ -1,166 +1,40 @@
1
1
  {
2
2
  "name": "@michaelthielemann/kestrel",
3
- "version": "4.1.3",
4
- "description": "A slim, collection-driven Nuxt 4 CMS meta-layer with a runtime schema engine. Add `extends: ['@michaelthielemann/kestrel']`, define collections, and the database migrates itself.",
3
+ "version": "5.0.0",
4
+ "description": "Kestrel core: contract registry, boot check and pipeline runner. Ships no contracts and no modules.",
5
+ "type": "module",
5
6
  "license": "Apache-2.0",
6
- "author": "Michael Thielemann <283621694+MichaelThielemann@users.noreply.github.com>",
7
- "homepage": "https://github.com/MichaelThielemann/kestrel#readme",
8
- "repository": {
9
- "type": "git",
10
- "url": "git+https://github.com/MichaelThielemann/kestrel.git"
7
+ "engines": {
8
+ "node": ">=22.13.0"
11
9
  },
12
- "bugs": "https://github.com/MichaelThielemann/kestrel/issues",
13
- "keywords": [
14
- "cms",
15
- "nuxt",
16
- "nuxt-layer",
17
- "static-site-generator",
18
- "drizzle",
19
- "sqlite",
20
- "headless-cms"
21
- ],
22
- "type": "module",
23
- "main": "./nuxt.config.ts",
24
10
  "bin": {
25
- "kestrel": "./scripts/kestrel.mjs"
11
+ "kestrel": "./dist/cli.js"
26
12
  },
27
- "publishConfig": {
28
- "access": "public",
29
- "provenance": true
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ },
18
+ "./*": {
19
+ "types": "./dist/*.d.ts",
20
+ "default": "./dist/*.js"
21
+ }
30
22
  },
31
- "//publish": "Consumed as a meta-layer: `extends: ['@michaelthielemann/kestrel']`. `main` points at the layer's nuxt.config so the BARE specifier resolves — without it c12 can't resolve the package, silently drops the whole layer (\"Cannot extend config from …\"), and every auto-import/component vanishes. Use `main`, NOT `exports` (exports would gate subpaths like `@michaelthielemann/kestrel/scripts/hash-password.mjs` + the deep config-type import); `bin` resolves by path from the package root, so it coexists with `main` untouched. `files` ships the entry + sub-layers + operator scripts + the `kestrel init` templates; the `!**/*.test.ts` globs strip tests — they are GLOBAL, so nothing under `templates/` may be named `*.test.ts` or it vanishes from the tarball. Template dotfiles are `_`-prefixed for the same class of reason: npm strips a literal `.gitignore` from a tarball and then applies it, taking its listed siblings with it. Releases run from the tag workflow via npm trusted publishing (OIDC), so no npm token exists anywhere.",
32
23
  "files": [
33
- "NOTICE",
34
- "nuxt.config.ts",
35
- "kestrel.config.ts",
36
- "layers",
37
- "scripts",
38
- "templates",
39
- "!**/*.test.ts",
40
- "!**/*.dom.test.ts",
41
- "!**/*.nuxt.test.ts"
24
+ "dist",
25
+ "README.md"
42
26
  ],
43
- "engines": {
44
- "node": ">=22.12.0"
45
- },
46
- "//lint": "The root eslint.config.mjs imports the generated playground/.nuxt/eslint.config.mjs (Nuxt-aware: auto-import globals for every layer + extension) — `nuxt prepare` must run first or that import 404s on a fresh checkout. Cheap on a warm .nuxt cache, so it's chained rather than a separate step.",
47
27
  "dependencies": {
48
- "@babel/parser": "^7.29.8",
49
- "@internationalized/date": "^3.12.3",
50
- "@tiptap/extension-highlight": "^3.29.2",
51
- "@tiptap/extension-subscript": "^3.29.2",
52
- "@tiptap/extension-superscript": "^3.29.2",
53
- "@tiptap/extension-text-align": "^3.29.2",
54
- "@tiptap/pm": "^3.29.2",
55
- "@tiptap/starter-kit": "^3.29.2",
56
- "@tiptap/vue-3": "^3.29.2",
57
- "@types/better-sqlite3": "^7.6.13",
58
- "@types/jsdom": "^28.0.3",
59
- "@types/sanitize-html": "^2.16.1",
60
- "aws4fetch": "^1.0.20",
61
- "better-sqlite3": "^12.11.1",
62
- "dompurify": "^3.4.13",
63
- "drizzle-orm": "^0.45.2",
64
- "drizzle-zod": "^0.8.3",
65
- "effect": "~3.22.1",
66
- "exifr": "^7.1.3",
67
- "file-type": "^22.0.1",
68
- "jsdom": "^29.1.1",
69
- "nanoid": "^5.1.16",
70
- "nitropack": "^2.13.4",
71
- "nuxt": "^4.5.2",
72
- "reka-ui": "^2.10.1",
73
- "sanitize-html": "^2.17.6",
74
- "sass": "^1.102.0",
75
- "sharp": "^0.35.3",
76
- "thumbhash": "^0.1.1",
77
- "typescript": "^6.0.3",
78
- "zod": "^4.4.3",
79
- "@michaelthielemann/kestrel-access": "0.1.0",
80
- "@michaelthielemann/kestrel-auth": "0.1.0",
81
- "@michaelthielemann/kestrel-contracts": "0.1.0",
82
- "@michaelthielemann/kestrel-collections": "0.1.0",
83
- "@michaelthielemann/kestrel-core": "0.1.0",
84
- "@michaelthielemann/kestrel-delivery-live": "0.1.0",
85
- "@michaelthielemann/kestrel-delivery-static": "0.1.0",
86
- "@michaelthielemann/kestrel-fields": "0.1.0",
87
- "@michaelthielemann/kestrel-media": "0.1.0",
88
- "@michaelthielemann/kestrel-publishing": "0.1.0"
89
- },
90
- "//peers": "Framework singletons the layer sources import by bare specifier. Peers, not dependencies: a second copy breaks identity (two Vue instances; an h3 v1 `createError` handed to an h3 v2 error handler, which surfaces our 400/404/409 as bare 500s). All optional, because kestrel's own `nuxt` dependency already supplies them transitively — the entry pins identity where a copy exists rather than demanding one, and a required peer would ERESOLVE-fail npm installs on any Nuxt whose bundled range we did not anticipate. Each is also a devDependency so this repo resolves it without auto-install-peers writing a phantom root dependency into the lockfile.",
91
- "peerDependencies": {
92
- "@nuxt/kit": "^4.4.0",
93
- "h3": "^1.15.0 || ^2.0.0",
94
- "vue": "^3.5.0",
95
- "vue-router": "^4.5.0 || ^5.0.0"
28
+ "zod": "^3.23.8"
96
29
  },
97
- "peerDependenciesMeta": {
98
- "@nuxt/kit": {
99
- "optional": true
100
- },
101
- "h3": {
102
- "optional": true
103
- },
104
- "vue": {
105
- "optional": true
106
- },
107
- "vue-router": {
108
- "optional": true
109
- }
110
- },
111
- "devDependencies": {
112
- "@arethetypeswrong/cli": "^0.18.5",
113
- "@fast-check/vitest": "^0.4.1",
114
- "@microsoft/api-extractor": "^7.58.13",
115
- "@nuxt/eslint": "^1.9.0",
116
- "@nuxt/kit": "^4.5.2",
117
- "@nuxt/test-utils": "^4.1.0",
118
- "@stryker-mutator/core": "^10.0.0",
119
- "@stryker-mutator/vitest-runner": "^10.0.0",
120
- "@vitejs/plugin-vue": "^6.0.8",
121
- "@vitest/coverage-v8": "4.1.10",
122
- "@vue/test-utils": "^2.4.11",
123
- "drizzle-kit": "^0.31.10",
124
- "eslint": "^9.39.1",
125
- "eslint-plugin-boundaries": "^7.2.0",
126
- "eslint-plugin-jsdoc": "^64.2.1",
127
- "eslint-plugin-tsdoc": "^0.5.2",
128
- "eslint-plugin-vuejs-accessibility": "^2.4.1",
129
- "fast-check": "^4.9.0",
130
- "h3": "^1.15.11",
131
- "happy-dom": "^20.11.2",
132
- "playwright-core": "^1.62.1",
133
- "publint": "^0.3.24",
134
- "typedoc": "^0.28.20",
135
- "vitest": "^4.1.10",
136
- "vitest-openapi": "^1.0.3",
137
- "vue": "3.5.35",
138
- "vue-router": "^5.2.0",
139
- "vue-tsc": "^3.3.9"
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/MichaelThielemann/kestrel.git",
33
+ "directory": "packages/core"
140
34
  },
35
+ "homepage": "https://github.com/MichaelThielemann/kestrel#readme",
36
+ "bugs": "https://github.com/MichaelThielemann/kestrel/issues",
141
37
  "scripts": {
142
- "dev": "nuxt dev",
143
- "build": "nuxt build",
144
- "build:packages": "pnpm -F @michaelthielemann/kestrel-contracts build && pnpm -F @michaelthielemann/kestrel-core build && pnpm -F @michaelthielemann/kestrel-fields build && pnpm -F @michaelthielemann/kestrel-auth build && pnpm -F @michaelthielemann/kestrel-access build && pnpm -F @michaelthielemann/kestrel-media build && pnpm -F @michaelthielemann/kestrel-collections build && pnpm -F @michaelthielemann/kestrel-publishing build && pnpm -F @michaelthielemann/kestrel-delivery-live build && pnpm -F @michaelthielemann/kestrel-delivery-static build",
145
- "preview": "nuxt preview",
146
- "generate": "nuxt generate",
147
- "db:generate": "drizzle-kit generate",
148
- "db:migrate": "drizzle-kit migrate",
149
- "test": "vitest run",
150
- "test:coverage": "node scripts/coverage-all.mjs",
151
- "test:watch": "vitest",
152
- "test:e2e": "vitest run --config vitest.e2e.config.ts",
153
- "test:nuxt": "vitest run --config vitest.nuxt.config.ts",
154
- "test:packages": "pnpm --filter \"@michaelthielemann/kestrel-*\" -r test",
155
- "typecheck": "node scripts/typecheck.mjs",
156
- "typecheck:packages": "pnpm --filter \"@michaelthielemann/kestrel-*\" -r typecheck",
157
- "lint": "nuxt prepare playground && eslint .",
158
- "docs:api": "nuxt prepare playground && typedoc",
159
- "api:check": "pnpm -F @michaelthielemann/kestrel-contracts build && api-extractor run -c packages/kestrel-contracts/api-extractor.json && pnpm -F @michaelthielemann/kestrel-core build && api-extractor run -c packages/kestrel-core/api-extractor.json && api-extractor run -c packages/kestrel-core/api-extractor.client.json && pnpm -F @michaelthielemann/kestrel-fields build && api-extractor run -c packages/kestrel-fields/api-extractor.json && api-extractor run -c packages/kestrel-fields/api-extractor.client.json && pnpm -F @michaelthielemann/kestrel-auth build && api-extractor run -c packages/kestrel-auth/api-extractor.json && pnpm -F @michaelthielemann/kestrel-access build && api-extractor run -c packages/kestrel-access/api-extractor.json && pnpm -F @michaelthielemann/kestrel-media build && api-extractor run -c packages/kestrel-media/api-extractor.json && pnpm -F @michaelthielemann/kestrel-collections build && api-extractor run -c packages/kestrel-collections/api-extractor.json && pnpm -F @michaelthielemann/kestrel-publishing build && api-extractor run -c packages/kestrel-publishing/api-extractor.json && pnpm -F @michaelthielemann/kestrel-delivery-live build && api-extractor run -c packages/kestrel-delivery-live/api-extractor.json && pnpm -F @michaelthielemann/kestrel-delivery-static build && api-extractor run -c packages/kestrel-delivery-static/api-extractor.json",
160
- "api:update": "pnpm -F @michaelthielemann/kestrel-contracts build && api-extractor run --local -c packages/kestrel-contracts/api-extractor.json && pnpm -F @michaelthielemann/kestrel-core build && api-extractor run --local -c packages/kestrel-core/api-extractor.json && api-extractor run --local -c packages/kestrel-core/api-extractor.client.json && pnpm -F @michaelthielemann/kestrel-fields build && api-extractor run --local -c packages/kestrel-fields/api-extractor.json && api-extractor run --local -c packages/kestrel-fields/api-extractor.client.json && pnpm -F @michaelthielemann/kestrel-auth build && api-extractor run --local -c packages/kestrel-auth/api-extractor.json && pnpm -F @michaelthielemann/kestrel-access build && api-extractor run --local -c packages/kestrel-access/api-extractor.json && pnpm -F @michaelthielemann/kestrel-media build && api-extractor run --local -c packages/kestrel-media/api-extractor.json && pnpm -F @michaelthielemann/kestrel-collections build && api-extractor run --local -c packages/kestrel-collections/api-extractor.json && pnpm -F @michaelthielemann/kestrel-publishing build && api-extractor run --local -c packages/kestrel-publishing/api-extractor.json && pnpm -F @michaelthielemann/kestrel-delivery-live build && api-extractor run --local -c packages/kestrel-delivery-live/api-extractor.json && pnpm -F @michaelthielemann/kestrel-delivery-static build && api-extractor run --local -c packages/kestrel-delivery-static/api-extractor.json",
161
- "pkg:lint": "publint . && pnpm -F @michaelthielemann/kestrel-contracts build && publint packages/kestrel-contracts && attw --pack packages/kestrel-contracts --profile esm-only && pnpm -F @michaelthielemann/kestrel-core build && publint packages/kestrel-core && attw --pack packages/kestrel-core --profile esm-only && pnpm -F @michaelthielemann/kestrel-fields build && publint packages/kestrel-fields && attw --pack packages/kestrel-fields --profile esm-only && pnpm -F @michaelthielemann/kestrel-auth build && publint packages/kestrel-auth && attw --pack packages/kestrel-auth --profile esm-only && pnpm -F @michaelthielemann/kestrel-access build && publint packages/kestrel-access && attw --pack packages/kestrel-access --profile esm-only && pnpm -F @michaelthielemann/kestrel-media build && publint packages/kestrel-media && attw --pack packages/kestrel-media --profile esm-only && pnpm -F @michaelthielemann/kestrel-collections build && publint packages/kestrel-collections && attw --pack packages/kestrel-collections --profile esm-only && pnpm -F @michaelthielemann/kestrel-publishing build && publint packages/kestrel-publishing && attw --pack packages/kestrel-publishing --profile esm-only && pnpm -F @michaelthielemann/kestrel-delivery-live build && publint packages/kestrel-delivery-live && attw --pack packages/kestrel-delivery-live --profile esm-only && pnpm -F @michaelthielemann/kestrel-delivery-static build && publint packages/kestrel-delivery-static && attw --pack packages/kestrel-delivery-static --profile esm-only",
162
- "ci:consumer-template": "node scripts/consumer-template-ci.mjs",
163
- "dashboard": "npx tsx scripts/dashboard.mjs",
164
- "hash-password": "node scripts/hash-password.mjs"
38
+ "build": "tsc -b tsconfig.json"
165
39
  }
166
40
  }
package/NOTICE DELETED
@@ -1,12 +0,0 @@
1
- Kestrel
2
- Copyright 2026 Michael Thielemann
3
-
4
- Licensed under the Apache License, Version 2.0 (the "License"); you may not use
5
- this software except in compliance with the License. You may obtain a copy of
6
- the License in the LICENSE file distributed with this software, or at
7
-
8
- http://www.apache.org/licenses/LICENSE-2.0
9
-
10
- This product is provided free of charge and is developed and made available
11
- outside of any commercial activity. It comes with NO WARRANTY and no commercial
12
- support; see sections 7 and 8 of the License.
package/kestrel.config.ts DELETED
@@ -1,28 +0,0 @@
1
- import type { KestrelConfig } from '@michaelthielemann/kestrel-core'
2
-
3
- /**
4
- * Central Kestrel configuration (everything except auth/session, which stays env-only because secrets
5
- * don't belong in committed config). This one file is the single source: it flows into `nuxt.config`'s
6
- * `kestrel: { … }`, into `drizzle.config` (so `drizzle-kit` uses the same DB), and into the server at
7
- * runtime. Precedence per setting: the matching `KESTREL_*` env var → a value here → a built-in default.
8
- *
9
- * This instance's non-auth config lives here (not in `.env`) so `nuxt.config`, `drizzle-kit` and the
10
- * runtime server all derive the SAME values from one place — in particular the same SQLite file, so
11
- * `pnpm dev`, `pnpm build` and a migration never drift onto different DBs (they set no `KESTREL_*`, so
12
- * this file wins). `.env` keeps only the auth/session secrets. An explicit `KESTREL_*` env var OVERRIDES
13
- * this file — the escape-hatch for a per-environment value or an isolated test (the e2e suite sets
14
- * `KESTREL_DB` / `KESTREL_MEDIA_LOCAL_DIR` / `KESTREL_SITE_URL` per run). The static-publish `output`
15
- * target is left to its defaults (local `.data/published`, auto-publish on).
16
- */
17
- export default {
18
- db: '.data/dev.sqlite',
19
- siteUrl: 'http://localhost:3000',
20
- locales: ['en', 'de'],
21
- primaryLocale: 'en',
22
- media: {
23
- driver: 'local',
24
- uploadDir: '.data/uploads',
25
- baseUrl: '/uploads',
26
- maxBytes: 26214400, // 25 MiB
27
- },
28
- } satisfies KestrelConfig
@@ -1,5 +0,0 @@
1
- // Authorization (authz) layer — the access guard, policy, CSRF + (later) the grant registry. Split out of
2
- // `auth` (which keeps authentication: session/password/login). Composes AFTER `auth`: the guard verifies the
3
- // session (via auth's `verifySession`/`sessionSettings`) to derive the request principal, then evaluates the
4
- // policy. Keeping authz separate is the seam a future User&Roles extension plugs into.
5
- export default defineNuxtConfig({})
@@ -1,52 +0,0 @@
1
- // Stage-level IP gate: the ENFORCEMENT of an allow-list Kestrel provides; the list itself is consumer
2
- // config (env `KESTREL_IP_ALLOWLIST`, typically only on non-public DEV/EDIT stages). Runs before the API
3
- // access-guard (00. sort prefix) and covers every route this app defines — the admin UI, public preview,
4
- // and API. It does NOT cover assets Nitro serves itself ahead of this stack (`/_nuxt/**`, and `/uploads`
5
- // when `media.driver: 'local'`) — the `00.` prefix only orders files within this layer, while Nitro's own
6
- // static handler is unconditionally unshifted to the front of the whole handler list. `off` when no list
7
- // is set → zero cost on PROD/local.
8
- import { clientIp } from '@michaelthielemann/kestrel-auth'
9
- import { allowlistConfig, ipAllowed, ipv4ToInt, isRendererContext, isStageGatePassedContext, markStageGatePassed } from '@michaelthielemann/kestrel-access'
10
-
11
- // A listener that never shows the gate a usable client address blocks everything, which looks like an
12
- // outage rather than a misconfiguration unless we say once what to set.
13
- let warnedUnresolvable = false
14
-
15
- export default defineEventHandler((event) => {
16
- const { mode, cidrs } = allowlistConfig()
17
- if (mode === 'off') return
18
-
19
- // The runtime publisher renders the public site via a nested in-process $fetch (renderer principal), and
20
- // build-time prerender the same. Neither is an external client — exempt them or publishing 403s itself.
21
- if (import.meta.prerender === true || isRendererContext()) return
22
-
23
- // Nitro's other in-process sub-requests (the SSR page's own `$fetch`, the preview) run through this
24
- // stack on a mocked socket with no peer, so they could never match a CIDR — gating them would 403 the
25
- // very pages the gate protects. They are recognised by the async context of the request that spawned
26
- // them: a missing peer alone is NOT a pass, or a listener that reports none for anybody (a unix domain
27
- // socket) would turn the gate into a silent no-op.
28
- if (isStageGatePassedContext() && !event.node?.req?.socket?.remoteAddress) return
29
-
30
- const ip = clientIp(event)
31
- if (ipAllowed(ip, cidrs)) {
32
- // The in-process sub-requests this one spawns inherit the decision (see above).
33
- markStageGatePassed()
34
- return
35
- }
36
-
37
- if (ipv4ToInt(ip) === null && !warnedUnresolvable) {
38
- warnedUnresolvable = true
39
- console.warn(`[ip-allowlist] cannot resolve an IPv4 client address (got "${ip}") — such requests are blocked in enforce mode; set KESTREL_TRUST_PROXY when a reverse proxy fronts this listener`)
40
- }
41
-
42
- if (mode === 'log') {
43
- // Calibration mode: never block, just surface what the gate WOULD reject so the operator can confirm
44
- // the resolved client IP + KESTREL_TRUST_PROXY depth before switching to enforce.
45
- console.warn(`[ip-allowlist] would block ${ip} — ${event.method} ${event.path} (xff=${getRequestHeader(event, 'x-forwarded-for') ?? '-'})`)
46
- // Nothing is blocked in this mode, so the report is about the external request, not the in-process
47
- // renders it goes on to spawn.
48
- markStageGatePassed()
49
- return
50
- }
51
- throw createError({ statusCode: 403, statusMessage: 'Forbidden' })
52
- })
@@ -1,32 +0,0 @@
1
- import { Forbidden, Unauthorized } from '@michaelthielemann/kestrel-contracts'
2
- import { toHttpError } from '@michaelthielemann/kestrel-core'
3
- import { refreshAuthSession } from '@michaelthielemann/kestrel-auth'
4
- import { claimedByPipelineRoute, resolveEventPrincipal } from '@michaelthielemann/kestrel-access'
5
-
6
- export default defineEventHandler((event) => {
7
- if (!event.path.startsWith('/api/')) return // default-deny applies to the API only
8
-
9
- // Authorization itself belongs to the pipeline the URL names — its `access`/`csrf`/`ipAllowlist`
10
- // declarations, evaluated by the engine before step 1. Only the principal is resolved here, because the
11
- // gates need it and because the session refresh below is a property of the request, not of the route.
12
- event.context.principal = resolveEventPrincipal(event)
13
-
14
- // Default-deny for everything no pipeline claims — an unknown name, a wrong verb, a stray route a
15
- // consumer mounted under /api without declaring who may run it. The admin falls through instead, so the
16
- // router can still answer accurately (404 for an unknown pipeline, 405 for the wrong verb) without that
17
- // answer confirming to an anonymous prober which pipelines exist. For the same reason an anonymous
18
- // caller gets the access gate's 401 here, not a 403 — a 401/403 split would itself be the oracle.
19
- if (!claimedByPipelineRoute(getMethod(event), event.path) && event.context.principal.role !== 'admin') {
20
- // toHttpError is the same single translation every pipeline-thrown KestrelError goes through
21
- // (core/server/api/[...path].ts's catch) — this middleware runs before any pipeline, so it calls the
22
- // map directly instead of growing a second one.
23
- if (event.context.principal.role === 'anonymous') {
24
- throw toHttpError(new Unauthorized({ reason: 'Authentication required' }))
25
- }
26
- throw toHttpError(new Forbidden({ reason: 'Forbidden' }))
27
- }
28
-
29
- // Sliding-expiry: any authenticated API activity refreshes the session so it stays alive while in use and
30
- // auto-expires after `maxAge` of inactivity (idle logout). No-op for anonymous/public requests.
31
- refreshAuthSession(event)
32
- })
@@ -1,7 +0,0 @@
1
- import type { Principal } from '@michaelthielemann/kestrel-access'
2
-
3
- declare module 'h3' {
4
- interface H3EventContext {
5
- principal?: Principal
6
- }
7
- }
@@ -1,212 +0,0 @@
1
- <script setup lang="ts">
2
- import {
3
- DropdownMenuRoot, DropdownMenuTrigger, DropdownMenuPortal, DropdownMenuContent,
4
- DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuRadioGroup, DropdownMenuRadioItem,
5
- DropdownMenuItemIndicator, DropdownMenuItem,
6
- } from 'reka-ui'
7
-
8
- // The rail's pseudo-account area: an avatar that opens an account menu (UI-language + sign out).
9
- const { t, lang } = useT()
10
- const { logout } = useAuth()
11
-
12
- // Single-user admin for now — static initials. A natural seam for a real display name later.
13
- const initials = 'AD'
14
-
15
- // reka's DropdownMenuRadioGroup emits the broad `AcceptableValue`; accept `unknown` and coerce to our lang.
16
- function selectLang(value: unknown) { lang.value = value as string }
17
- function signOut() { logout() }
18
-
19
- // Exposed for the component test: the menu itself is teleported (not rendered by happy-dom), so the
20
- // load-bearing handlers are asserted directly.
21
- defineExpose({ lang, selectLang, signOut })
22
- </script>
23
-
24
- <template>
25
- <DropdownMenuRoot>
26
- <DropdownMenuTrigger as-child>
27
- <button type="button" class="rail-account__trigger" :aria-label="t('account.label')" :title="t('account.label')">
28
- <span class="rail-account__avatar" aria-hidden="true">{{ initials }}</span>
29
- <span class="rail-account__name rail__label">{{ t('account.name') }}</span>
30
- <KestrelUiIcon name="chevron-down" class="rail-account__caret rail__label" size="1rem" />
31
- </button>
32
- </DropdownMenuTrigger>
33
-
34
- <DropdownMenuPortal>
35
- <DropdownMenuContent
36
- class="rail-account__menu"
37
- side="top"
38
- align="start"
39
- :side-offset="6"
40
- :collision-padding="8"
41
- >
42
- <DropdownMenuLabel class="rail-account__head">
43
- <span class="rail-account__avatar rail-account__avatar--sm" aria-hidden="true">{{ initials }}</span>
44
- <span class="rail-account__head-name">{{ t('account.name') }}</span>
45
- </DropdownMenuLabel>
46
-
47
- <DropdownMenuSeparator class="rail-account__sep" />
48
-
49
- <DropdownMenuLabel class="rail-account__group-label">{{ t('lang.label') }}</DropdownMenuLabel>
50
- <DropdownMenuRadioGroup :model-value="lang" @update:model-value="selectLang">
51
- <DropdownMenuRadioItem
52
- v-for="l in ADMIN_LANGS"
53
- :key="l"
54
- :value="l"
55
- class="rail-account__item"
56
- >
57
- <span class="rail-account__check">
58
- <DropdownMenuItemIndicator>
59
- <KestrelUiIcon name="check" size="1rem" />
60
- </DropdownMenuItemIndicator>
61
- </span>
62
- <span>{{ l.toUpperCase() }}</span>
63
- </DropdownMenuRadioItem>
64
- </DropdownMenuRadioGroup>
65
-
66
- <DropdownMenuSeparator class="rail-account__sep" />
67
-
68
- <DropdownMenuItem class="rail-account__item rail-account__item--danger" @select="signOut">
69
- <KestrelUiIcon name="log-out" size="1rem" class="rail-account__item-icon" />
70
- <span>{{ t('nav.signOut') }}</span>
71
- </DropdownMenuItem>
72
- </DropdownMenuContent>
73
- </DropdownMenuPortal>
74
- </DropdownMenuRoot>
75
- </template>
76
-
77
- <style lang="scss">
78
- // Not scoped: Reka teleports DropdownMenuContent to <body>, where a scoped data-v selector no longer
79
- // matches (same reason UiMenu's styles are global). The trigger lives in the rail; keeping the whole
80
- // component's styles global is simplest and consistent with the other portaled overlays.
81
- .rail-account__trigger {
82
- display: flex;
83
- align-items: center;
84
- gap: var(--space-3);
85
- width: 100%;
86
- padding: var(--space-2) var(--space-3);
87
- margin-bottom: var(--space-1);
88
- border: 0;
89
- border-radius: var(--radius-md);
90
- background: transparent;
91
- color: var(--color-text);
92
- font: inherit;
93
- font-size: var(--text-sm);
94
- text-align: left;
95
- cursor: pointer;
96
-
97
- &:hover {
98
- background: var(--color-hover);
99
- }
100
- &:focus-visible {
101
- outline: 2px solid var(--color-focus);
102
- outline-offset: -2px;
103
- }
104
- &[data-state='open'] {
105
- background: var(--color-active, var(--color-surface-2));
106
- }
107
- }
108
-
109
- .rail-account__avatar {
110
- display: inline-flex;
111
- align-items: center;
112
- justify-content: center;
113
- flex-shrink: 0;
114
- width: 1.75rem;
115
- height: 1.75rem;
116
- border-radius: var(--radius-full);
117
- border: 1px solid var(--color-border-strong, var(--color-border));
118
- background: var(--color-surface-2);
119
- color: var(--color-text);
120
- font-size: var(--text-xs, 0.75rem);
121
- font-weight: var(--weight-bold);
122
- letter-spacing: 0.02em;
123
-
124
- &--sm {
125
- width: 1.5rem;
126
- height: 1.5rem;
127
- }
128
- }
129
-
130
- .rail-account__name {
131
- font-weight: var(--weight-medium);
132
- }
133
- .rail-account__caret {
134
- margin-left: auto;
135
- flex-shrink: 0;
136
- color: var(--color-text-muted);
137
- }
138
-
139
- // Menu surface — mirrors .ui-menu so the account dropdown reads like the rest of the admin chrome.
140
- .rail-account__menu {
141
- min-width: 13rem;
142
- background: var(--color-surface);
143
- border: 1px solid var(--color-border);
144
- border-radius: var(--radius-md);
145
- box-shadow: var(--shadow-md);
146
- padding: var(--space-1);
147
- z-index: var(--z-dropdown);
148
- }
149
-
150
- .rail-account__head {
151
- display: flex;
152
- align-items: center;
153
- gap: var(--space-2);
154
- padding: var(--space-2) var(--space-2);
155
- }
156
- .rail-account__head-name {
157
- font-size: var(--text-sm);
158
- font-weight: var(--weight-medium);
159
- }
160
-
161
- .rail-account__group-label {
162
- padding: var(--space-1) var(--space-2);
163
- font-size: var(--text-xs, 0.75rem);
164
- font-weight: var(--weight-medium);
165
- color: var(--color-text-muted);
166
- }
167
-
168
- .rail-account__sep {
169
- height: 1px;
170
- margin: var(--space-1) calc(var(--space-1) * -1);
171
- background: var(--color-border);
172
- }
173
-
174
- .rail-account__item {
175
- display: flex;
176
- align-items: center;
177
- gap: var(--space-2);
178
- width: 100%;
179
- padding: var(--space-1) var(--space-2);
180
- border-radius: var(--radius-sm);
181
- font-size: var(--text-sm);
182
- color: var(--color-text);
183
- cursor: pointer;
184
- user-select: none;
185
-
186
- &[data-highlighted] {
187
- background: var(--color-hover);
188
- outline: none;
189
- }
190
- &--danger {
191
- color: var(--color-danger);
192
- }
193
- }
194
-
195
- .rail-account__check {
196
- display: inline-flex;
197
- align-items: center;
198
- justify-content: center;
199
- width: 1rem;
200
- flex-shrink: 0;
201
- color: var(--color-primary);
202
- }
203
- .rail-account__item-icon {
204
- flex-shrink: 0;
205
- }
206
-
207
- // Collapsed rail: drop the label + caret and centre the avatar (mirrors .rail__item's collapsed rule).
208
- .admin--rail-collapsed .rail-account__trigger {
209
- justify-content: center;
210
- gap: 0;
211
- }
212
- </style>
@@ -1,44 +0,0 @@
1
- <script setup lang="ts">
2
- import { resolveLocalized } from '../../../ui/app/utils/localized'
3
-
4
- const { t, lang } = useT()
5
- const { collections, load } = useCollections()
6
- const { collapsed } = useRailCollapsed()
7
- const route = useRoute()
8
- try {
9
- await load()
10
- } catch {
11
- // Nav is non-critical chrome; a failed fetch leaves it empty rather than breaking the layout.
12
- }
13
- </script>
14
-
15
- <template>
16
- <nav class="admin-nav" :aria-label="t('nav.collections')">
17
- <NuxtLink
18
- v-for="c in (collections ?? []).filter((c) => c.nav !== false)"
19
- :key="c.name"
20
- :to="`/admin/${c.name}`"
21
- class="admin-nav__link rail__item"
22
- :class="{ 'router-link-active': isNavItemActive(route.path, `/admin/${c.name}`) }"
23
- :title="collapsed ? (resolveLocalized(c.label?.plural, lang) ?? c.name) : undefined"
24
- >
25
- <KestrelUiIcon :name="c.icon ?? 'file-text'" class="rail__icon" size="1.25rem" />
26
- <span class="rail__label">{{ resolveLocalized(c.label?.plural, lang) ?? c.name }}</span>
27
- </NuxtLink>
28
- </nav>
29
- </template>
30
-
31
- <style lang="scss">
32
- .admin-nav {
33
- display: flex;
34
- flex-direction: column;
35
- gap: var(--space-1);
36
-
37
- // Layout, colour and active state come from the shared .rail__item (admin layout).
38
- // Only the cosmetic capitalisation is nav-specific — kept CSS-only so the rendered
39
- // text node stays the raw collection name (e.g. "settings").
40
- &__link {
41
- text-transform: capitalize;
42
- }
43
- }
44
- </style>