@michaelthielemann/kestrel 4.1.2 → 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
@@ -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>