@michaelthielemann/kestrel 4.1.3 → 5.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (342) hide show
  1. package/README.md +26 -201
  2. package/dist/boot.d.ts +45 -0
  3. package/dist/boot.js +284 -0
  4. package/dist/cast.d.ts +3 -0
  5. package/dist/cast.js +6 -0
  6. package/dist/catalogue.d.ts +11 -0
  7. package/dist/catalogue.js +1 -0
  8. package/dist/cli.d.ts +2 -0
  9. package/dist/cli.js +55 -0
  10. package/dist/context.d.ts +62 -0
  11. package/dist/context.js +61 -0
  12. package/dist/dataflow.d.ts +3 -0
  13. package/dist/dataflow.js +65 -0
  14. package/dist/defineConfig.d.ts +162 -0
  15. package/dist/defineConfig.js +42 -0
  16. package/dist/defineContract.d.ts +12 -0
  17. package/dist/defineContract.js +16 -0
  18. package/dist/defineModule.d.ts +75 -0
  19. package/dist/defineModule.js +9 -0
  20. package/dist/definePipeline.d.ts +7 -0
  21. package/dist/definePipeline.js +11 -0
  22. package/dist/errors.d.ts +26 -0
  23. package/dist/errors.js +48 -0
  24. package/dist/index.d.ts +19 -0
  25. package/dist/index.js +14 -0
  26. package/dist/load.d.ts +7 -0
  27. package/dist/load.js +79 -0
  28. package/dist/logger.d.ts +18 -0
  29. package/dist/logger.js +36 -0
  30. package/dist/registry.d.ts +14 -0
  31. package/dist/registry.js +87 -0
  32. package/dist/result.d.ts +19 -0
  33. package/dist/result.js +19 -0
  34. package/dist/runner.d.ts +29 -0
  35. package/dist/runner.js +133 -0
  36. package/dist/schema.d.ts +7 -0
  37. package/dist/schema.js +142 -0
  38. package/dist/sort.d.ts +2 -0
  39. package/dist/sort.js +50 -0
  40. package/dist/testing/runPipeline.d.ts +9 -0
  41. package/dist/testing/runPipeline.js +22 -0
  42. package/dist/triggers/allowlist.d.ts +9 -0
  43. package/dist/triggers/allowlist.js +49 -0
  44. package/dist/triggers/cron.d.ts +10 -0
  45. package/dist/triggers/cron.js +44 -0
  46. package/dist/triggers/cronMatch.d.ts +10 -0
  47. package/dist/triggers/cronMatch.js +68 -0
  48. package/dist/triggers/http.d.ts +59 -0
  49. package/dist/triggers/http.js +300 -0
  50. package/package.json +25 -151
  51. package/NOTICE +0 -12
  52. package/kestrel.config.ts +0 -28
  53. package/layers/access/nuxt.config.ts +0 -5
  54. package/layers/access/server/middleware/00.ip-allowlist.ts +0 -52
  55. package/layers/access/server/middleware/access-guard.ts +0 -32
  56. package/layers/access/server/types.d.ts +0 -7
  57. package/layers/admin/app/components/AdminAccount.vue +0 -212
  58. package/layers/admin/app/components/AdminNav.vue +0 -44
  59. package/layers/admin/app/components/BlockFields.vue +0 -67
  60. package/layers/admin/app/components/BlockPreview.vue +0 -413
  61. package/layers/admin/app/components/BlockTree.vue +0 -433
  62. package/layers/admin/app/components/BlocksBody.vue +0 -163
  63. package/layers/admin/app/components/CollectionDeleteDialog.vue +0 -38
  64. package/layers/admin/app/components/CollectionEditor.vue +0 -324
  65. package/layers/admin/app/components/CollectionList.vue +0 -429
  66. package/layers/admin/app/components/CollectionListBulkBar.vue +0 -62
  67. package/layers/admin/app/components/CollectionListColumnsPanel.vue +0 -37
  68. package/layers/admin/app/components/CollectionListFilterPanel.vue +0 -145
  69. package/layers/admin/app/components/CollectionListPager.vue +0 -70
  70. package/layers/admin/app/components/CollectionListTable.vue +0 -347
  71. package/layers/admin/app/components/EditorStatus.vue +0 -211
  72. package/layers/admin/app/components/EditorUnsupported.vue +0 -28
  73. package/layers/admin/app/components/FieldsBody.vue +0 -23
  74. package/layers/admin/app/components/LocaleBar.vue +0 -107
  75. package/layers/admin/app/components/PageFields.vue +0 -129
  76. package/layers/admin/app/components/PageFieldsPane.vue +0 -39
  77. package/layers/admin/app/components/SeoFields.vue +0 -210
  78. package/layers/admin/app/components/SingletonEditor.vue +0 -93
  79. package/layers/admin/app/composables/useAuth.ts +0 -68
  80. package/layers/admin/app/composables/useBlockTree.ts +0 -95
  81. package/layers/admin/app/composables/useBlocks.ts +0 -7
  82. package/layers/admin/app/composables/useCachedResource.ts +0 -35
  83. package/layers/admin/app/composables/useCollectionOps.ts +0 -102
  84. package/layers/admin/app/composables/useCollections.ts +0 -6
  85. package/layers/admin/app/composables/useContentLocales.ts +0 -23
  86. package/layers/admin/app/composables/useEditForm.ts +0 -402
  87. package/layers/admin/app/composables/useLinkResolver.ts +0 -40
  88. package/layers/admin/app/composables/useListBatchActions.ts +0 -78
  89. package/layers/admin/app/composables/useListColumns.ts +0 -53
  90. package/layers/admin/app/composables/useListFilterDraft.ts +0 -129
  91. package/layers/admin/app/composables/useListRows.ts +0 -75
  92. package/layers/admin/app/composables/useListSelection.ts +0 -28
  93. package/layers/admin/app/composables/useListUrlState.ts +0 -53
  94. package/layers/admin/app/composables/usePendingPublishPoll.ts +0 -15
  95. package/layers/admin/app/composables/usePreviewViewport.ts +0 -30
  96. package/layers/admin/app/composables/usePublishStatus.ts +0 -107
  97. package/layers/admin/app/composables/useRailCollapsed.ts +0 -14
  98. package/layers/admin/app/composables/useTheme.ts +0 -36
  99. package/layers/admin/app/composables/useToolbarPanel.ts +0 -38
  100. package/layers/admin/app/composables/useUnsavedGuard.ts +0 -22
  101. package/layers/admin/app/layouts/admin.vue +0 -266
  102. package/layers/admin/app/middleware/admin-auth.ts +0 -4
  103. package/layers/admin/app/pages/admin/[collection]/[id].vue +0 -203
  104. package/layers/admin/app/pages/admin/[collection]/index.vue +0 -62
  105. package/layers/admin/app/pages/admin/index.vue +0 -118
  106. package/layers/admin/app/pages/admin/login.vue +0 -68
  107. package/layers/admin/app/pages/admin/media.vue +0 -28
  108. package/layers/admin/app/pages/admin/references.vue +0 -146
  109. package/layers/admin/app/plugins/admin-style-guard.client.ts +0 -76
  110. package/layers/admin/app/plugins/reauth.client.ts +0 -20
  111. package/layers/admin/app/utils/admin-style-guard.ts +0 -20
  112. package/layers/admin/app/utils/block-tree.ts +0 -214
  113. package/layers/admin/app/utils/collection-ops.ts +0 -69
  114. package/layers/admin/app/utils/dead-refs.ts +0 -30
  115. package/layers/admin/app/utils/edit-form.ts +0 -267
  116. package/layers/admin/app/utils/editor-expose.ts +0 -36
  117. package/layers/admin/app/utils/editor-form-context.ts +0 -82
  118. package/layers/admin/app/utils/editor-registry.ts +0 -20
  119. package/layers/admin/app/utils/field-keys.ts +0 -11
  120. package/layers/admin/app/utils/list-cell.ts +0 -46
  121. package/layers/admin/app/utils/list-columns.ts +0 -142
  122. package/layers/admin/app/utils/list-query.ts +0 -96
  123. package/layers/admin/app/utils/nav-active.ts +0 -9
  124. package/layers/admin/app/utils/populate-blocks.ts +0 -288
  125. package/layers/admin/app/utils/preview-channel.ts +0 -62
  126. package/layers/admin/app/utils/preview-viewport.ts +0 -80
  127. package/layers/admin/app/utils/reauth.ts +0 -67
  128. package/layers/admin/app/utils/record-title.ts +0 -15
  129. package/layers/admin/app/utils/register-builtin-editors.ts +0 -9
  130. package/layers/admin/app/utils/safe-redirect.ts +0 -12
  131. package/layers/admin/nuxt.config.ts +0 -20
  132. package/layers/auth/nuxt.config.ts +0 -1
  133. package/layers/auth/server/plugins/01.register-auth-pipelines.ts +0 -7
  134. package/layers/collections/nuxt.config.ts +0 -1
  135. package/layers/collections/server/plugins/01.register.ts +0 -29
  136. package/layers/collections/server/plugins/02.register-relation-populate.ts +0 -18
  137. package/layers/core/app/composables/layouts.ts +0 -5
  138. package/layers/core/app/utils/field-factories.ts +0 -129
  139. package/layers/core/app/utils/layouts.ts +0 -38
  140. package/layers/core/modules/auto-discovery/extract-block.ts +0 -179
  141. package/layers/core/modules/auto-discovery/index.ts +0 -112
  142. package/layers/core/modules/auto-discovery/package-registry.ts +0 -19
  143. package/layers/core/modules/auto-discovery/scan.ts +0 -170
  144. package/layers/core/modules/auto-discovery/virtual.d.ts +0 -20
  145. package/layers/core/modules/component-namespace/index.ts +0 -44
  146. package/layers/core/modules/component-namespace/shared.ts +0 -24
  147. package/layers/core/modules/kestrel/app-shell.ts +0 -55
  148. package/layers/core/modules/kestrel/auto-imports.ts +0 -44
  149. package/layers/core/modules/kestrel/index.ts +0 -144
  150. package/layers/core/modules/plugin-order/index.ts +0 -69
  151. package/layers/core/modules/plugin-order/plugin-order.ts +0 -273
  152. package/layers/core/nuxt.config.ts +0 -10
  153. package/layers/core/server/api/[...path].ts +0 -98
  154. package/layers/core/server/plugins/00.config.ts +0 -12
  155. package/layers/core/server/plugins/00.migrate.ts +0 -12
  156. package/layers/core/server/plugins/01.register-introspection-pipeline.ts +0 -5
  157. package/layers/core/server/plugins/01.register-openapi-pipeline.ts +0 -5
  158. package/layers/core/server/plugins/01.register-tooling-pipelines.ts +0 -5
  159. package/layers/core/server/plugins/02.schema-sync.ts +0 -58
  160. package/layers/core/server/plugins/04.outbox-worker.ts +0 -31
  161. package/layers/core/server/plugins/05.reindex-refs.ts +0 -5
  162. package/layers/core/server/routes/__kestrel/dashboard.get.ts +0 -29
  163. package/layers/core/server/tasks/db/migrate-module.ts +0 -83
  164. package/layers/core/server/tasks/db/migrate-revisions.ts +0 -20
  165. package/layers/core/server/tasks/db/migrate.ts +0 -50
  166. package/layers/core/server/utils/server-config.ts +0 -81
  167. package/layers/fields/nuxt.config.ts +0 -1
  168. package/layers/fields/server/plugins/01.register-blocks-pipeline.ts +0 -7
  169. package/layers/fields/server/plugins/01.register-field-populate.ts +0 -11
  170. package/layers/media/app/components/KestrelImg.vue +0 -94
  171. package/layers/media/app/components/MediaConflictDialog.vue +0 -35
  172. package/layers/media/app/components/MediaDeleteDialog.vue +0 -52
  173. package/layers/media/app/components/MediaGrid.vue +0 -113
  174. package/layers/media/app/components/MediaImage.vue +0 -70
  175. package/layers/media/app/components/MediaLibrary.vue +0 -456
  176. package/layers/media/app/components/MediaNewFolderDialog.vue +0 -32
  177. package/layers/media/app/components/MediaPathBar.vue +0 -163
  178. package/layers/media/app/components/MediaPicker.vue +0 -19
  179. package/layers/media/app/components/MediaRenameDialog.vue +0 -32
  180. package/layers/media/app/components/MediaTable.vue +0 -75
  181. package/layers/media/app/components/MediaThumb.vue +0 -25
  182. package/layers/media/app/components/MediaToolbar.vue +0 -90
  183. package/layers/media/app/components/MediaUploadDialog.vue +0 -87
  184. package/layers/media/app/components/MediaViewer.vue +0 -137
  185. package/layers/media/app/components/field/Media.vue +0 -263
  186. package/layers/media/app/composables/useBlurUp.ts +0 -55
  187. package/layers/media/app/composables/useMediaClipboard.ts +0 -14
  188. package/layers/media/app/composables/useMediaContextMenu.ts +0 -56
  189. package/layers/media/app/composables/useMediaDnd.ts +0 -56
  190. package/layers/media/app/composables/useMediaLibrary.ts +0 -152
  191. package/layers/media/app/composables/useMediaOps.ts +0 -50
  192. package/layers/media/app/composables/useMediaRelocate.ts +0 -39
  193. package/layers/media/app/composables/useMediaResolver.ts +0 -50
  194. package/layers/media/app/composables/useMediaUpload.ts +0 -93
  195. package/layers/media/app/composables/useMediaVariant.ts +0 -34
  196. package/layers/media/app/plugins/field-media.client.ts +0 -6
  197. package/layers/media/app/utils/ai-disclosure.ts +0 -19
  198. package/layers/media/app/utils/dnd.ts +0 -69
  199. package/layers/media/app/utils/library.ts +0 -81
  200. package/layers/media/app/utils/ops.ts +0 -60
  201. package/layers/media/app/utils/picture-model.ts +0 -125
  202. package/layers/media/app/utils/thumbhash.ts +0 -19
  203. package/layers/media/app/utils/variant-name.ts +0 -20
  204. package/layers/media/nuxt.config.ts +0 -7
  205. package/layers/media/server/middleware/ondemand-variants.ts +0 -38
  206. package/layers/media/server/plugins/01.register-media-pipelines.ts +0 -7
  207. package/layers/media/server/plugins/02.register-media.ts +0 -22
  208. package/layers/media/server/plugins/04.variant-capture.ts +0 -27
  209. package/layers/media/server/plugins/05.media-cleanup.ts +0 -5
  210. package/layers/media/server/tasks/media/backfill.ts +0 -17
  211. package/layers/public/app/app.vue +0 -5
  212. package/layers/public/app/components/BlockRenderer.vue +0 -56
  213. package/layers/public/app/components/KestrelLink.vue +0 -26
  214. package/layers/public/app/components/KestrelPreviewBridge.vue +0 -133
  215. package/layers/public/app/composables/public-page.ts +0 -24
  216. package/layers/public/app/error.vue +0 -12
  217. package/layers/public/app/layouts/default.vue +0 -5
  218. package/layers/public/app/pages/[...slug].vue +0 -239
  219. package/layers/public/app/pages/__kestrel/preview.vue +0 -46
  220. package/layers/public/app/utils/block-edit-context.ts +0 -16
  221. package/layers/public/app/utils/json-ld.ts +0 -139
  222. package/layers/public/app/utils/link-href.ts +0 -46
  223. package/layers/public/app/utils/page-head.ts +0 -84
  224. package/layers/public/app/utils/page-layout.ts +0 -18
  225. package/layers/public/app/utils/preview-protocol.ts +0 -117
  226. package/layers/public/app/utils/public-route.ts +0 -46
  227. package/layers/public/app/utils/site-head.ts +0 -43
  228. package/layers/public/modules/deploy-output/index.ts +0 -80
  229. package/layers/public/modules/prerender-routes/discover.ts +0 -37
  230. package/layers/public/modules/prerender-routes/index.ts +0 -88
  231. package/layers/public/modules/prerender-routes/routes.ts +0 -16
  232. package/layers/public/modules/prune-media/index.ts +0 -48
  233. package/layers/public/modules/prune-media/prune-media.ts +0 -81
  234. package/layers/public/nuxt.config.ts +0 -14
  235. package/layers/public/server/delivery-live/serve.ts +0 -111
  236. package/layers/public/server/middleware/delivery-live-catchall.ts +0 -7
  237. package/layers/public/server/pipelines/index.ts +0 -23
  238. package/layers/public/server/plugins/00.ensure-snapshot-triggers.ts +0 -14
  239. package/layers/public/server/plugins/01.register-public-pipelines.ts +0 -7
  240. package/layers/public/server/plugins/02.register-links.ts +0 -16
  241. package/layers/public/server/plugins/03.redirects.ts +0 -55
  242. package/layers/public/server/plugins/05.plan-publish.ts +0 -5
  243. package/layers/public/server/plugins/zz.publish.ts +0 -127
  244. package/layers/public/server/routes/llms-full.txt.get.ts +0 -99
  245. package/layers/public/server/routes/llms.txt.get.ts +0 -66
  246. package/layers/public/server/routes/redirects.json.get.ts +0 -58
  247. package/layers/public/server/routes/robots.txt.get.ts +0 -11
  248. package/layers/public/server/routes/sitemap.xml.get.ts +0 -79
  249. package/layers/public/server/tasks/publish/run.ts +0 -32
  250. package/layers/public/server/utils/publish/render-live.ts +0 -26
  251. package/layers/ui/app/assets/scss/_base.scss +0 -16
  252. package/layers/ui/app/assets/scss/_datepicker.scss +0 -82
  253. package/layers/ui/app/assets/scss/_mixins.scss +0 -50
  254. package/layers/ui/app/assets/scss/_reset.scss +0 -65
  255. package/layers/ui/app/assets/scss/_tokens.scss +0 -183
  256. package/layers/ui/app/assets/scss/main.scss +0 -4
  257. package/layers/ui/app/components/field/Boolean.vue +0 -30
  258. package/layers/ui/app/components/field/Choice.vue +0 -73
  259. package/layers/ui/app/components/field/Datetime.vue +0 -98
  260. package/layers/ui/app/components/field/DeadRefNote.vue +0 -28
  261. package/layers/ui/app/components/field/Json.vue +0 -62
  262. package/layers/ui/app/components/field/Layout.vue +0 -138
  263. package/layers/ui/app/components/field/Link.vue +0 -112
  264. package/layers/ui/app/components/field/LinkInternalPicker.vue +0 -95
  265. package/layers/ui/app/components/field/Number.vue +0 -30
  266. package/layers/ui/app/components/field/Relation.vue +0 -49
  267. package/layers/ui/app/components/field/Renderer.vue +0 -30
  268. package/layers/ui/app/components/field/Repeater.vue +0 -460
  269. package/layers/ui/app/components/field/Richtext.vue +0 -26
  270. package/layers/ui/app/components/field/Slug.vue +0 -65
  271. package/layers/ui/app/components/field/Text.vue +0 -35
  272. package/layers/ui/app/components/field/Unsupported.vue +0 -34
  273. package/layers/ui/app/components/ui/Alert.vue +0 -50
  274. package/layers/ui/app/components/ui/Brand.vue +0 -22
  275. package/layers/ui/app/components/ui/Button.vue +0 -116
  276. package/layers/ui/app/components/ui/ButtonGroup.vue +0 -96
  277. package/layers/ui/app/components/ui/Checkbox.vue +0 -53
  278. package/layers/ui/app/components/ui/CheckboxGroup.vue +0 -51
  279. package/layers/ui/app/components/ui/Combobox.vue +0 -345
  280. package/layers/ui/app/components/ui/DatePicker.vue +0 -92
  281. package/layers/ui/app/components/ui/DateRangePicker.vue +0 -100
  282. package/layers/ui/app/components/ui/Dialog.vue +0 -107
  283. package/layers/ui/app/components/ui/EmptyState.vue +0 -46
  284. package/layers/ui/app/components/ui/Field.vue +0 -58
  285. package/layers/ui/app/components/ui/Fieldset.vue +0 -65
  286. package/layers/ui/app/components/ui/Icon.vue +0 -44
  287. package/layers/ui/app/components/ui/Menu.vue +0 -54
  288. package/layers/ui/app/components/ui/NumberInput.vue +0 -175
  289. package/layers/ui/app/components/ui/Richtext.vue +0 -141
  290. package/layers/ui/app/components/ui/RichtextToolbar.vue +0 -251
  291. package/layers/ui/app/components/ui/Select.vue +0 -52
  292. package/layers/ui/app/components/ui/TextInput.vue +0 -111
  293. package/layers/ui/app/components/ui/Textarea.vue +0 -39
  294. package/layers/ui/app/components/ui/TimeInput.vue +0 -18
  295. package/layers/ui/app/components/ui/Toasts.vue +0 -103
  296. package/layers/ui/app/components/ui/Tooltip.vue +0 -45
  297. package/layers/ui/app/composables/useAdminLang.ts +0 -8
  298. package/layers/ui/app/composables/useDragReorder.ts +0 -41
  299. package/layers/ui/app/composables/useEchoGuard.ts +0 -28
  300. package/layers/ui/app/composables/useLinkField.ts +0 -70
  301. package/layers/ui/app/composables/useRecordOptions.ts +0 -84
  302. package/layers/ui/app/composables/useRepeater.ts +0 -99
  303. package/layers/ui/app/composables/useT.ts +0 -32
  304. package/layers/ui/app/composables/useToast.ts +0 -46
  305. package/layers/ui/app/i18n/de.ts +0 -432
  306. package/layers/ui/app/i18n/en.ts +0 -434
  307. package/layers/ui/app/utils/boolean-field.ts +0 -10
  308. package/layers/ui/app/utils/field-component.ts +0 -22
  309. package/layers/ui/app/utils/field-empty.ts +0 -21
  310. package/layers/ui/app/utils/field-registry.ts +0 -36
  311. package/layers/ui/app/utils/humanize.ts +0 -14
  312. package/layers/ui/app/utils/icons.ts +0 -143
  313. package/layers/ui/app/utils/iso-date.ts +0 -18
  314. package/layers/ui/app/utils/localized.ts +0 -13
  315. package/layers/ui/app/utils/record-context.ts +0 -15
  316. package/layers/ui/app/utils/reorder.ts +0 -8
  317. package/layers/ui/app/utils/richtext-preserve-class.ts +0 -31
  318. package/layers/ui/app/utils/useFieldA11y.ts +0 -40
  319. package/layers/ui/nuxt.config.ts +0 -9
  320. package/nuxt.config.ts +0 -108
  321. package/scripts/consumer-template-ci.mjs +0 -160
  322. package/scripts/copy-create-payload.mjs +0 -49
  323. package/scripts/coverage-all.mjs +0 -163
  324. package/scripts/dashboard.mjs +0 -89
  325. package/scripts/hash-password.mjs +0 -29
  326. package/scripts/kestrel.mjs +0 -240
  327. package/scripts/lib/cli.mjs +0 -129
  328. package/scripts/lib/coverage-model.mjs +0 -51
  329. package/scripts/lib/password.mjs +0 -19
  330. package/scripts/lib/scaffold.mjs +0 -205
  331. package/scripts/port-graph.mjs +0 -350
  332. package/scripts/typecheck.mjs +0 -62
  333. package/templates/starter/README.md +0 -65
  334. package/templates/starter/_env.example +0 -17
  335. package/templates/starter/_gitignore +0 -26
  336. package/templates/starter/_package.json +0 -22
  337. package/templates/starter/app/app.vue +0 -7
  338. package/templates/starter/app/blocks/Prose.vue +0 -13
  339. package/templates/starter/app/layouts/default.vue +0 -6
  340. package/templates/starter/nuxt.config.ts +0 -23
  341. package/templates/starter/pnpm-workspace.yaml +0 -8
  342. package/templates/starter/tsconfig.json +0 -3
@@ -1,266 +0,0 @@
1
- <script setup lang="ts">
2
- // The admin design system (tokens / reset / base) loads HERE — with the admin-layout chunk — rather than
3
- // app-globally, so it stays scoped to /admin and never leaks Kestrel's reset onto a consumer's public site
4
- // (which ships its own CSS, e.g. Bootstrap). Every admin route uses this layout, so coverage is complete.
5
- import '../../../ui/app/assets/scss/main.scss'
6
-
7
- const { authenticated } = useAuth()
8
- const { collapsed, toggle: toggleRail } = useRailCollapsed()
9
- const { theme, toggle: toggleTheme } = useTheme()
10
- const { t } = useT()
11
-
12
- // Bind the resolved theme onto <html> so the admin palette (and teleported widgets) pick it up, and
13
- // re-assert the root font-size while the admin is mounted. The admin is entirely rem-based and assumes
14
- // the browser-default root; a consumer that sets `html { font-size: … }` on its public site would
15
- // otherwise rescale the whole panel (rem resolves against the shared root, which no admin-side selector
16
- // can override). An inline style beats a consumer stylesheet rule in the cascade without needing
17
- // `!important`; `initial` restores the user-agent default (respecting the visitor's own font-size
18
- // preference). unhead removes both attributes when the admin layout unmounts, so the public site keeps
19
- // its own root. In dev the style guard also strips the consumer sheet outright (see
20
- // admin-style-guard.client.ts); this reassert is the production-safe half of the defense.
21
- // No admin page sets its own <title> today, so this fallback is the only one every route gets — a
22
- // per-page useHead still wins (titleTemplate only fires when a title is actually set).
23
- useHead(() => ({
24
- title: 'Kestrel',
25
- titleTemplate: (title) => (title && title !== 'Kestrel' ? `${title} · Kestrel` : 'Kestrel'),
26
- htmlAttrs: { 'data-theme': theme.value, style: 'font-size: initial' },
27
- }))
28
- </script>
29
-
30
- <template>
31
- <div class="admin" :class="{ 'admin--rail-collapsed': collapsed }">
32
- <aside v-if="authenticated" class="admin__rail">
33
- <div class="rail__head">
34
- <NuxtLink to="/admin" class="rail__brand" aria-label="Kestrel">
35
- <KestrelUiBrand />
36
- <span class="rail__brand-word rail__label">kestrel</span>
37
- </NuxtLink>
38
- <button
39
- type="button"
40
- class="rail__toggle"
41
- :aria-label="collapsed ? t('a11y.expandSidebar') : t('a11y.collapseSidebar')"
42
- @click="toggleRail"
43
- >
44
- <KestrelUiIcon :name="collapsed ? 'panel-left-open' : 'panel-left-close'" size="1.25rem" />
45
- </button>
46
- </div>
47
-
48
- <div class="rail__nav">
49
- <NuxtLink
50
- to="/admin"
51
- class="rail__item rail__item--dashboard"
52
- :title="collapsed ? t('nav.dashboard') : undefined"
53
- >
54
- <KestrelUiIcon name="home" class="rail__icon" size="1.25rem" />
55
- <span class="rail__label">{{ t('nav.dashboard') }}</span>
56
- </NuxtLink>
57
-
58
- <KestrelAdminNav />
59
- </div>
60
-
61
- <div class="rail__foot">
62
- <KestrelAdminAccount />
63
- <button
64
- type="button"
65
- class="rail__item"
66
- :title="collapsed ? (theme === 'dark' ? t('theme.light') : t('theme.dark')) : undefined"
67
- :aria-label="theme === 'dark' ? t('a11y.toLight') : t('a11y.toDark')"
68
- @click="toggleTheme"
69
- >
70
- <KestrelUiIcon :name="theme === 'dark' ? 'sun' : 'moon'" class="rail__icon" size="1.25rem" />
71
- <span class="rail__label">{{ theme === 'dark' ? t('theme.light') : t('theme.dark') }}</span>
72
- </button>
73
- </div>
74
- </aside>
75
-
76
- <main class="admin__main"><slot /></main>
77
- <KestrelUiToasts />
78
- </div>
79
- </template>
80
-
81
- <style lang="scss">
82
- .admin {
83
- display: flex;
84
- align-items: flex-start;
85
- min-height: 100svh;
86
- background: var(--color-bg);
87
-
88
- &__rail {
89
- position: sticky;
90
- top: 0;
91
- flex: 0 0 var(--rail-width);
92
- width: var(--rail-width);
93
- height: 100svh;
94
- display: flex;
95
- flex-direction: column;
96
- background: var(--color-rail-bg);
97
- border-right: 1px solid var(--color-border);
98
- transition:
99
- flex-basis var(--motion-base) var(--ease-standard),
100
- width var(--motion-base) var(--ease-standard);
101
- }
102
-
103
- // App-shell: the main column is locked to the viewport height and never scrolls itself — each page
104
- // (and, in the editor, each of its panes) owns its own scroll region instead.
105
- &__main {
106
- flex: 1 1 auto;
107
- min-width: 0;
108
- height: 100svh;
109
- overflow: hidden;
110
- display: flex;
111
- flex-direction: column;
112
- padding: var(--space-4);
113
- }
114
-
115
- // Scroll-ownership convention — every admin page follows it. The page section handed this
116
- // `flex:1 1 auto; min-height:0` is a flex column that NEVER scrolls itself (overflow:hidden); it keeps
117
- // its header/toolbar fixed (flex:0 0 auto) and delegates vertical scroll to exactly ONE inner region
118
- // (flex:1 1 auto; min-height:0; overflow:auto). One unbroken min-height:0 chain reaches that region, so
119
- // nothing else clips (inline popovers stay visible). Those regions, by page: the list table
120
- // (.list__scroll), the flat/blocks editor body (.editor__flat / .editor3 panes), the media grid
121
- // (.media-library__items) and the references table (.refs__scroll).
122
- &__main > * {
123
- flex: 1 1 auto;
124
- min-height: 0;
125
- }
126
-
127
- &--rail-collapsed &__rail {
128
- flex-basis: var(--rail-width-collapsed);
129
- width: var(--rail-width-collapsed);
130
- }
131
- }
132
-
133
- .rail {
134
- &__head {
135
- display: flex;
136
- align-items: center;
137
- justify-content: space-between;
138
- gap: var(--space-2);
139
- padding: var(--space-4) var(--space-3);
140
- }
141
-
142
- &__brand {
143
- display: inline-flex;
144
- align-items: center;
145
- gap: var(--space-2);
146
- min-width: 0;
147
- color: var(--color-text);
148
- text-decoration: none;
149
- font-weight: var(--weight-bold);
150
- border-radius: var(--radius-sm);
151
-
152
- &:focus-visible {
153
- outline: 2px solid var(--color-focus);
154
- outline-offset: -2px;
155
- }
156
- }
157
-
158
- &__toggle {
159
- display: inline-flex;
160
- align-items: center;
161
- justify-content: center;
162
- flex-shrink: 0;
163
- padding: var(--space-1);
164
- border: 0;
165
- border-radius: var(--radius-sm);
166
- background: transparent;
167
- color: var(--color-text-muted);
168
- cursor: pointer;
169
-
170
- &:hover {
171
- background: var(--color-hover);
172
- color: var(--color-text);
173
- }
174
- &:focus-visible {
175
- outline: 2px solid var(--color-focus);
176
- outline-offset: -2px;
177
- }
178
- }
179
-
180
- &__nav {
181
- flex: 1 1 auto;
182
- overflow-y: auto;
183
- display: flex;
184
- flex-direction: column;
185
- gap: var(--space-1);
186
- padding: var(--space-2) var(--space-3);
187
- }
188
-
189
- // Shared item used by the static links, the collection links (AdminNav) and Sign out.
190
- &__item {
191
- display: flex;
192
- align-items: center;
193
- gap: var(--space-3);
194
- width: 100%;
195
- padding: var(--space-2) var(--space-3);
196
- border: 0;
197
- border-radius: var(--radius-md);
198
- background: transparent;
199
- color: var(--color-text);
200
- font: inherit;
201
- font-size: var(--text-sm);
202
- text-align: left;
203
- text-decoration: none;
204
- cursor: pointer;
205
-
206
- &:hover {
207
- background: var(--color-hover);
208
- }
209
- &:focus-visible {
210
- outline: 2px solid var(--color-focus);
211
- outline-offset: -2px;
212
- }
213
- // Calm active state: a neutral fill (not a tinted pill) with the icon carrying a restrained accent.
214
- &.router-link-active {
215
- background: var(--color-active, var(--color-surface-2));
216
- color: var(--color-text);
217
- font-weight: var(--weight-medium);
218
- }
219
- }
220
-
221
- &__item.router-link-active &__icon {
222
- color: var(--color-primary);
223
- }
224
-
225
- &__icon {
226
- flex-shrink: 0;
227
- }
228
-
229
- &__label {
230
- flex: 1 1 auto;
231
- min-width: 0;
232
- overflow: hidden;
233
- text-overflow: ellipsis;
234
- white-space: nowrap;
235
- }
236
-
237
- &__foot {
238
- padding: var(--space-3);
239
- border-top: 1px solid var(--color-border);
240
- }
241
-
242
- }
243
-
244
- // Collapsed state: hide labels (kept in the DOM for a11y + tests), center icons.
245
- .admin--rail-collapsed {
246
- .rail__label {
247
- position: absolute;
248
- width: 1px;
249
- height: 1px;
250
- padding: 0;
251
- margin: -1px;
252
- overflow: hidden;
253
- clip: rect(0, 0, 0, 0);
254
- white-space: nowrap;
255
- border: 0;
256
- }
257
- .rail__item {
258
- justify-content: center;
259
- gap: 0;
260
- }
261
- .rail__head {
262
- flex-direction: column;
263
- align-items: center;
264
- }
265
- }
266
- </style>
@@ -1,4 +0,0 @@
1
- export default defineNuxtRouteMiddleware(async (to) => {
2
- if (await useAuth().ensureSession()) return
3
- return navigateTo(`/admin/login?redirect=${encodeURIComponent(to.fullPath)}`)
4
- })
@@ -1,203 +0,0 @@
1
- <script setup lang="ts">
2
- import { resolveLocalized } from '../../../../../ui/app/utils/localized'
3
- import type { EditorExpose } from '../../../utils/editor-expose'
4
- import type { BatchDeleteReport } from '../../../utils/collection-ops'
5
-
6
- definePageMeta({ layout: 'admin', middleware: 'admin-auth', key: (route) => route.fullPath })
7
-
8
- const route = useRoute()
9
- const collection = route.params.collection as string
10
- const id = route.params.id as string
11
- // Content locale + translation group flow from the URL (set by the LocaleBar). The page `key` is the
12
- // full path, so switching locale remounts the editor cleanly with the new locale/group.
13
- const localeParam = computed(() => (typeof route.query.locale === 'string' ? route.query.locale.trim() || undefined : undefined))
14
- const group = computed(() => (typeof route.query.group === 'string' ? route.query.group.trim() || undefined : undefined))
15
- // Returning to the list keeps the locale we were editing in, so a multilingual workflow stays in context.
16
- const listPath = computed(() => `/admin/${collection}${localeParam.value ? `?locale=${localeParam.value}` : ''}`)
17
-
18
- const { t, lang } = useT()
19
- const toast = useToast()
20
- // Resolve the collection's display labels so the header reads "New Page" / "Posts", not the raw
21
- // (often plural) route param. Non-critical chrome — a failed load falls back to the param.
22
- const { load: loadCollections } = useCollections()
23
- const def = (await loadCollections().catch(() => [])).find((c) => c.name === collection) ?? null
24
- const singular = computed(() => resolveLocalized(def?.label?.singular, lang.value) ?? collection)
25
- const plural = computed(() => resolveLocalized(def?.label?.plural, lang.value) ?? collection)
26
- // The "create" heading prefers the collection's complete per-locale phrase (same one the list button
27
- // uses), so a German title reads "Neue Seite" / "Neuer Beitrag" — never "Neu: …".
28
- const newTitle = computed(() => resolveLocalized(def?.label?.new, lang.value) ?? t('editor.newRecord', { collection: singular.value }))
29
- // The header (where Delete also lives) drives Save by submitting the editor's form via its id, and reads
30
- // the editor's in-flight `saving` for the button state. `dirty` powers the unsaved-changes guard.
31
- const EDITOR_FORM_ID = 'record-editor'
32
- const editorRef = ref<EditorExpose | null>(null)
33
- const saving = computed(() => editorRef.value?.saving ?? false)
34
- // An id means nothing to an editor, so the heading shows the record's own title when it has one. A new (or
35
- // still untitled) record has nothing but its id, and the generic phrase is the only thing that reads well.
36
- const recordName = computed(() => editorRef.value?.recordTitle?.trim() ?? '')
37
- const hasRecordTitle = computed(() => id !== 'new' && recordName.value !== '')
38
- const heading = computed(() =>
39
- id === 'new'
40
- ? newTitle.value
41
- : hasRecordTitle.value
42
- ? recordName.value
43
- : t('editor.editRecord', { collection: singular.value, id }),
44
- )
45
-
46
- // The external-tab button opens a preview ticket when there are unsaved edits (no save, no publish),
47
- // otherwise the record's own URL — so the tooltip has to say which of the two you'd get.
48
- const previewTitle = computed(() => t(editorRef.value?.dirty ? 'editor.previewUnsaved' : 'editor.openInNewTab'))
49
-
50
- const skipGuard = ref(false)
51
-
52
- // Delete flows through the shared batch op + the same confirm dialog the list uses, so the delete logic
53
- // lives ONCE. On success the composable's onChanged navigates back to the list (bypassing the dirty guard).
54
- const { busy: deleting, error: deleteError, previewDelete, confirmDelete: runDelete } = useCollectionOps(collection, async () => {
55
- skipGuard.value = true
56
- await navigateTo(listPath.value)
57
- })
58
- const deleteOpen = ref(false)
59
- const deleteReport = ref<BatchDeleteReport | null>(null)
60
-
61
- function onSaved(record: unknown) {
62
- toast.success(t('toast.saved'))
63
- // Save only saves — it does not leave the editor (navigation is the back link / rail's job). The one
64
- // exception: a brand-new record switches to its own editor URL so further saves update it (id is now
65
- // known) instead of POSTing a duplicate.
66
- if (id === 'new') {
67
- const newId = (record as { id?: number | string } | null)?.id
68
- if (newId != null) {
69
- skipGuard.value = true
70
- return navigateTo(`/admin/${collection}/${newId}${localeParam.value ? `?locale=${localeParam.value}` : ''}`)
71
- }
72
- }
73
- }
74
-
75
- // Guard unsaved edits on BOTH route-record changes (→ list) AND same-record param/query nav (the
76
- // LocaleBar's edit-sibling / create-translation links reuse this `[id]` record → onBeforeRouteUpdate).
77
- useUnsavedGuard(() => editorRef.value?.dirty ?? false, () => t('editor.discardConfirm'), () => skipGuard.value)
78
-
79
- function toList() {
80
- return navigateTo(listPath.value)
81
- }
82
-
83
- // Open the confirm dialog, fetching the referrer aggregate for its warning. Best-effort: a failed lookup
84
- // still opens the dialog with a bare summary (deleting is never blocked on the warning).
85
- async function onDelete() {
86
- deleteReport.value = null
87
- deleteOpen.value = true
88
- try {
89
- deleteReport.value = await previewDelete([Number(id)])
90
- } catch {
91
- // The referrer lookup FAILED — mark the report unverified (checked:false) so the dialog shows the
92
- // "references could not be verified" caution instead of a reassuring referencedCount:0 (which would
93
- // read as a confirmed-safe delete).
94
- deleteReport.value = { count: 1, referencedCount: 0, referenced: [], checked: false }
95
- deleteError.value = null
96
- }
97
- }
98
- async function confirmDelete() {
99
- try {
100
- await runDelete([Number(id)])
101
- toast.success(t('toast.deleted'))
102
- deleteOpen.value = false
103
- // navigation back to the list is the composable's onChanged
104
- } catch {
105
- // deleteError stays surfaced in the dialog
106
- }
107
- }
108
- </script>
109
-
110
- <template>
111
- <section class="record">
112
- <div class="record__head">
113
- <NuxtLink :to="listPath" class="record__back">
114
- <KestrelUiIcon name="arrow-left" :size="16" />
115
- <span>{{ t('editor.back', { collection: plural }) }}</span>
116
- </NuxtLink>
117
- <h1 class="record__title" :class="{ 'record__title--generic': !hasRecordTitle }">{{ heading }}</h1>
118
- <div class="record__actions">
119
- <KestrelUiButton type="button" variant="ghost" size="sm" icon="undo" :disabled="saving || !editorRef?.canUndo" :title="t('history.undo')" :aria-label="t('history.undo')" @click="editorRef?.undo()" />
120
- <KestrelUiButton type="button" variant="ghost" size="sm" icon="redo" :disabled="saving || !editorRef?.canRedo" :title="t('history.redo')" :aria-label="t('history.redo')" @click="editorRef?.redo()" />
121
- <KestrelUiButton type="button" variant="ghost" size="sm" icon="external-link" :title="previewTitle" :aria-label="previewTitle" :loading="editorRef?.previewOpening" @click="editorRef?.openPreview()" />
122
- <KestrelUiButton type="button" variant="secondary" size="sm" icon="x" :disabled="saving" @click="toList">{{ t('common.cancel') }}</KestrelUiButton>
123
- <KestrelUiButton v-if="id !== 'new'" variant="danger" size="sm" icon="trash" :loading="deleting" @click="onDelete">{{ t('common.delete') }}</KestrelUiButton>
124
- <!-- A quarantined record is read-only: no Save, no Publish — there is nothing valid to write. -->
125
- <KestrelUiButton v-if="!editorRef?.quarantined" type="submit" :form="EDITOR_FORM_ID" variant="primary" size="sm" icon="check" :loading="saving">{{ t('common.save') }}</KestrelUiButton>
126
- <!-- Publishing is its own decision: Save persists, Publish writes the static page (ADR-0008). -->
127
- <KestrelUiButton v-if="editorRef?.canPublish !== false && !editorRef?.quarantined" type="button" variant="secondary" size="sm" icon="upload" :loading="editorRef?.publishing" :disabled="saving" @click="editorRef?.publish()">{{ t('common.publish') }}</KestrelUiButton>
128
- <KestrelEditorStatus class="record__ampel" :dirty="editorRef?.dirty ?? false" :saving="saving" :has-status="editorRef?.hasStatus ?? false" :status="editorRef?.status" :saved-status="editorRef?.savedStatus" :page-like="editorRef?.pageLike ?? false" :live="editorRef?.live" />
129
- </div>
130
- </div>
131
- <KestrelCollectionEditor ref="editorRef" :collection="collection" :id="id" :form-id="EDITOR_FORM_ID" :locale-param="localeParam" :group="group" :actions="false" @saved="onSaved" />
132
- <KestrelCollectionDeleteDialog
133
- :open="deleteOpen"
134
- :report="deleteReport"
135
- :busy="deleting"
136
- :error="deleteError"
137
- @update:open="deleteOpen = $event"
138
- @confirm="confirmDelete"
139
- />
140
- </section>
141
- </template>
142
-
143
- <style lang="scss">
144
- .record {
145
- display: flex;
146
- flex-direction: column;
147
- gap: var(--space-4);
148
- // The record shell stays fixed; the editor's panes own the scroll (see the admin layout).
149
- overflow: hidden;
150
-
151
- &__back {
152
- display: inline-flex;
153
- align-items: center;
154
- gap: var(--space-1);
155
- flex: 0 0 auto;
156
- font-size: var(--text-sm);
157
- color: var(--color-text-muted);
158
- text-decoration: none;
159
-
160
- &:hover {
161
- color: var(--color-text);
162
- }
163
- span {
164
- text-transform: capitalize;
165
- }
166
- }
167
- // Back link, heading and actions share ONE row so the editor keeps the vertical space for its panes.
168
- &__head {
169
- flex: 0 0 auto;
170
- display: flex;
171
- align-items: center;
172
- gap: var(--space-3);
173
- flex-wrap: wrap;
174
- }
175
- &__actions {
176
- display: flex;
177
- align-items: center;
178
- gap: var(--space-2);
179
- margin-left: auto;
180
- }
181
- // The status Ampel sits far-right, divided from the action buttons.
182
- &__ampel {
183
- margin-inline-start: var(--space-1);
184
- padding-inline-start: var(--space-3);
185
- border-inline-start: 1px solid var(--color-border);
186
- }
187
- &__title {
188
- font-size: var(--text-xl);
189
- font-weight: var(--weight-bold);
190
- // A long page title must not push the actions off the row.
191
- min-width: 0;
192
- overflow: hidden;
193
- text-overflow: ellipsis;
194
- white-space: nowrap;
195
-
196
- // Only the generic "Edit {collection} #{id}" / "New {collection}" phrase is title-cased — a real record
197
- // title is shown verbatim (capitalize would mangle e.g. "iPhone").
198
- &--generic {
199
- text-transform: capitalize;
200
- }
201
- }
202
- }
203
- </style>
@@ -1,62 +0,0 @@
1
- <script setup lang="ts">
2
- import { resolveLocalized } from '../../../../../ui/app/utils/localized'
3
-
4
- // Key on collection + locale ONLY (not the full path): a collection- or locale-change still remounts (which
5
- // resets committed filter/draft/page/selection from the clean target URL), but a sort/page/perPage/filter
6
- // query change must NOT remount — CollectionList derives that state from the URL in place, keeping the filter
7
- // panel open and focus intact. ([id].vue deliberately keeps `route.fullPath` — it needs locale/group/id.)
8
- definePageMeta({
9
- layout: 'admin',
10
- middleware: 'admin-auth',
11
- key: (route) => `${route.params.collection}::${typeof route.query.locale === 'string' ? route.query.locale : ''}`,
12
- })
13
-
14
- const route = useRoute()
15
- const collection = route.params.collection as string
16
- const localeParam = computed(() => (typeof route.query.locale === 'string' ? route.query.locale.trim() || undefined : undefined))
17
- const { primary } = useContentLocales()
18
-
19
- const { t, lang } = useT()
20
- const { load } = useCollections()
21
- const collections = await load()
22
- const def = collections.find((c) => c.name === collection) ?? null
23
- // The content locale the list browses: the ?locale or the primary, for a translatable collection only.
24
- const listLocale = computed(() => (def?.translatable ? (localeParam.value || primary) : undefined))
25
- </script>
26
-
27
- <template>
28
- <section class="collection">
29
- <p v-if="!def" class="collection__missing">{{ t('collection.unknown', { name: collection }) }}</p>
30
-
31
- <template v-else-if="def.mode === 'single'">
32
- <KestrelSingletonEditor :collection="collection" :title="resolveLocalized(def.label?.singular, lang) ?? collection" :locale-param="localeParam" />
33
- </template>
34
-
35
- <template v-else>
36
- <h1 class="collection__title">{{ resolveLocalized(def.label?.plural, lang) ?? collection }}</h1>
37
- <KestrelCollectionList :schema="def" :locale="listLocale" />
38
- </template>
39
- </section>
40
- </template>
41
-
42
- <style lang="scss">
43
- // Non-scrolling passthrough: hands its bounded height to whichever branch renders (the singleton
44
- // editor, or the list title + CollectionList). It never scrolls itself — the inner region does.
45
- .collection {
46
- display: flex;
47
- flex-direction: column;
48
- gap: var(--space-5);
49
- min-height: 0;
50
- overflow: hidden;
51
-
52
- &__title {
53
- flex: 0 0 auto;
54
- font-size: var(--text-xl);
55
- font-weight: var(--weight-bold);
56
- text-transform: capitalize;
57
- }
58
- &__missing {
59
- color: var(--color-text-muted);
60
- }
61
- }
62
- </style>
@@ -1,118 +0,0 @@
1
- <script setup lang="ts">
2
- import { resolveLocalized } from '../../../../ui/app/utils/localized'
3
-
4
- definePageMeta({ layout: 'admin', middleware: 'admin-auth' })
5
-
6
- const { t, lang } = useT()
7
- const { load } = useCollections()
8
- const collections = ref<Awaited<ReturnType<typeof load>>>([])
9
- try {
10
- // Match AdminNav: hide nav:false system collections (e.g. media_settings, the machine-managed variant
11
- // registry) — they are config stores, not content, and must not appear as clickable dashboard cards.
12
- collections.value = (await load()).filter((c) => c.nav !== false)
13
- } catch {
14
- // a transient /api/collections failure degrades to an empty dashboard, not an error page
15
- }
16
-
17
- // Broken-reference count for the alert banner — fire-and-forget so it never delays the dashboard, and a
18
- // failure (or a not-yet-built index) simply shows no banner.
19
- const brokenCount = ref(0)
20
- $fetch<unknown[]>('/api/brokenRefs')
21
- .then((r) => { brokenCount.value = r.length })
22
- .catch(() => { brokenCount.value = 0 })
23
- </script>
24
-
25
- <template>
26
- <section class="dash">
27
- <h1 class="dash__title">{{ t('dash.title') }}</h1>
28
- <p class="dash__lede">{{ t('dash.lede') }}</p>
29
- <NuxtLink v-if="brokenCount" to="/admin/references" class="dash__alert">
30
- <KestrelUiIcon name="triangle-alert" :size="18" />
31
- <span>{{ t('refs.dashAlert', { n: brokenCount }) }}</span>
32
- </NuxtLink>
33
- <KestrelUiEmptyState
34
- v-if="!collections.length"
35
- icon="file-text"
36
- :title="t('dash.empty.title')"
37
- :description="t('dash.empty.desc')"
38
- />
39
- <ul v-else class="dash__grid">
40
- <li v-for="c in collections" :key="c.name">
41
- <NuxtLink :to="`/admin/${c.name}`" class="dash__card">
42
- <KestrelUiIcon :name="c.icon ?? 'file-text'" class="dash__icon" size="1.5rem" />
43
- <span class="dash__name">{{ resolveLocalized(c.label?.plural, lang) ?? c.name }}</span>
44
- <span class="dash__mode">{{ c.mode === 'single' ? t('dash.mode.singleton') : t('dash.mode.collection') }}</span>
45
- </NuxtLink>
46
- </li>
47
- </ul>
48
- </section>
49
- </template>
50
-
51
- <style lang="scss">
52
- .dash {
53
- display: flex;
54
- flex-direction: column;
55
- gap: var(--space-3);
56
- overflow-y: auto;
57
-
58
- &__title {
59
- font-size: var(--text-xl);
60
- font-weight: var(--weight-bold);
61
- }
62
- &__lede {
63
- color: var(--color-text-muted);
64
- }
65
- // Stale-reference alert: a warning banner linking to the broken-references report (only when non-zero).
66
- &__alert {
67
- display: inline-flex;
68
- align-items: center;
69
- gap: var(--space-2);
70
- align-self: flex-start;
71
- padding: var(--space-2) var(--space-3);
72
- border: 1px solid var(--color-warning);
73
- border-radius: var(--radius-md);
74
- color: var(--color-warning-text); /* banner TEXT → 4.5:1 (the border keeps the 3:1 amber accent) */
75
- font-size: var(--text-sm);
76
- font-weight: var(--weight-medium);
77
- text-decoration: none;
78
- }
79
- &__grid {
80
- list-style: none;
81
- margin: var(--space-3) 0 0;
82
- padding: 0;
83
- display: grid;
84
- grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
85
- gap: var(--space-4);
86
- }
87
- &__card {
88
- display: flex;
89
- flex-direction: column;
90
- gap: var(--space-1);
91
- padding: var(--space-4);
92
- border: 1px solid var(--color-border);
93
- border-radius: var(--radius-lg);
94
- background: var(--color-surface);
95
- text-decoration: none;
96
- color: var(--color-text);
97
- transition: border-color var(--motion-fast) var(--ease-standard);
98
-
99
- &:hover {
100
- border-color: var(--color-border-strong);
101
- }
102
- &:focus-visible {
103
- border-color: var(--color-primary);
104
- }
105
- }
106
- &__icon {
107
- color: var(--color-primary);
108
- }
109
- &__name {
110
- font-weight: var(--weight-medium);
111
- text-transform: capitalize;
112
- }
113
- &__mode {
114
- font-size: var(--text-sm);
115
- color: var(--color-text-muted);
116
- }
117
- }
118
- </style>