@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.
- package/README.md +26 -201
- package/dist/boot.d.ts +45 -0
- package/dist/boot.js +274 -0
- package/dist/cast.d.ts +3 -0
- package/dist/cast.js +6 -0
- package/dist/catalogue.d.ts +11 -0
- package/dist/catalogue.js +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +55 -0
- package/dist/context.d.ts +62 -0
- package/dist/context.js +61 -0
- package/dist/dataflow.d.ts +3 -0
- package/dist/dataflow.js +65 -0
- package/dist/defineConfig.d.ts +157 -0
- package/dist/defineConfig.js +41 -0
- package/dist/defineContract.d.ts +12 -0
- package/dist/defineContract.js +16 -0
- package/dist/defineModule.d.ts +75 -0
- package/dist/defineModule.js +9 -0
- package/dist/definePipeline.d.ts +7 -0
- package/dist/definePipeline.js +11 -0
- package/dist/errors.d.ts +26 -0
- package/dist/errors.js +48 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +14 -0
- package/dist/load.d.ts +7 -0
- package/dist/load.js +79 -0
- package/dist/logger.d.ts +18 -0
- package/dist/logger.js +36 -0
- package/dist/registry.d.ts +14 -0
- package/dist/registry.js +87 -0
- package/dist/result.d.ts +19 -0
- package/dist/result.js +19 -0
- package/dist/runner.d.ts +29 -0
- package/dist/runner.js +133 -0
- package/dist/schema.d.ts +7 -0
- package/dist/schema.js +142 -0
- package/dist/sort.d.ts +2 -0
- package/dist/sort.js +50 -0
- package/dist/testing/runPipeline.d.ts +9 -0
- package/dist/testing/runPipeline.js +22 -0
- package/dist/triggers/cron.d.ts +10 -0
- package/dist/triggers/cron.js +44 -0
- package/dist/triggers/cronMatch.d.ts +10 -0
- package/dist/triggers/cronMatch.js +68 -0
- package/dist/triggers/http.d.ts +58 -0
- package/dist/triggers/http.js +292 -0
- package/package.json +25 -151
- package/NOTICE +0 -12
- package/kestrel.config.ts +0 -28
- package/layers/access/nuxt.config.ts +0 -5
- package/layers/access/server/middleware/00.ip-allowlist.ts +0 -52
- package/layers/access/server/middleware/access-guard.ts +0 -32
- package/layers/access/server/types.d.ts +0 -7
- package/layers/admin/app/components/AdminAccount.vue +0 -212
- package/layers/admin/app/components/AdminNav.vue +0 -44
- package/layers/admin/app/components/BlockFields.vue +0 -67
- package/layers/admin/app/components/BlockPreview.vue +0 -413
- package/layers/admin/app/components/BlockTree.vue +0 -433
- package/layers/admin/app/components/BlocksBody.vue +0 -163
- package/layers/admin/app/components/CollectionDeleteDialog.vue +0 -38
- package/layers/admin/app/components/CollectionEditor.vue +0 -324
- package/layers/admin/app/components/CollectionList.vue +0 -429
- package/layers/admin/app/components/CollectionListBulkBar.vue +0 -62
- package/layers/admin/app/components/CollectionListColumnsPanel.vue +0 -37
- package/layers/admin/app/components/CollectionListFilterPanel.vue +0 -145
- package/layers/admin/app/components/CollectionListPager.vue +0 -70
- package/layers/admin/app/components/CollectionListTable.vue +0 -347
- package/layers/admin/app/components/EditorStatus.vue +0 -211
- package/layers/admin/app/components/EditorUnsupported.vue +0 -28
- package/layers/admin/app/components/FieldsBody.vue +0 -23
- package/layers/admin/app/components/LocaleBar.vue +0 -107
- package/layers/admin/app/components/PageFields.vue +0 -129
- package/layers/admin/app/components/PageFieldsPane.vue +0 -39
- package/layers/admin/app/components/SeoFields.vue +0 -210
- package/layers/admin/app/components/SingletonEditor.vue +0 -93
- package/layers/admin/app/composables/useAuth.ts +0 -68
- package/layers/admin/app/composables/useBlockTree.ts +0 -95
- package/layers/admin/app/composables/useBlocks.ts +0 -7
- package/layers/admin/app/composables/useCachedResource.ts +0 -35
- package/layers/admin/app/composables/useCollectionOps.ts +0 -102
- package/layers/admin/app/composables/useCollections.ts +0 -6
- package/layers/admin/app/composables/useContentLocales.ts +0 -23
- package/layers/admin/app/composables/useEditForm.ts +0 -402
- package/layers/admin/app/composables/useLinkResolver.ts +0 -40
- package/layers/admin/app/composables/useListBatchActions.ts +0 -78
- package/layers/admin/app/composables/useListColumns.ts +0 -53
- package/layers/admin/app/composables/useListFilterDraft.ts +0 -129
- package/layers/admin/app/composables/useListRows.ts +0 -75
- package/layers/admin/app/composables/useListSelection.ts +0 -28
- package/layers/admin/app/composables/useListUrlState.ts +0 -53
- package/layers/admin/app/composables/usePendingPublishPoll.ts +0 -15
- package/layers/admin/app/composables/usePreviewViewport.ts +0 -30
- package/layers/admin/app/composables/usePublishStatus.ts +0 -107
- package/layers/admin/app/composables/useRailCollapsed.ts +0 -14
- package/layers/admin/app/composables/useTheme.ts +0 -36
- package/layers/admin/app/composables/useToolbarPanel.ts +0 -38
- package/layers/admin/app/composables/useUnsavedGuard.ts +0 -22
- package/layers/admin/app/layouts/admin.vue +0 -266
- package/layers/admin/app/middleware/admin-auth.ts +0 -4
- package/layers/admin/app/pages/admin/[collection]/[id].vue +0 -203
- package/layers/admin/app/pages/admin/[collection]/index.vue +0 -62
- package/layers/admin/app/pages/admin/index.vue +0 -118
- package/layers/admin/app/pages/admin/login.vue +0 -68
- package/layers/admin/app/pages/admin/media.vue +0 -28
- package/layers/admin/app/pages/admin/references.vue +0 -146
- package/layers/admin/app/plugins/admin-style-guard.client.ts +0 -76
- package/layers/admin/app/plugins/reauth.client.ts +0 -20
- package/layers/admin/app/utils/admin-style-guard.ts +0 -20
- package/layers/admin/app/utils/block-tree.ts +0 -214
- package/layers/admin/app/utils/collection-ops.ts +0 -69
- package/layers/admin/app/utils/dead-refs.ts +0 -30
- package/layers/admin/app/utils/edit-form.ts +0 -267
- package/layers/admin/app/utils/editor-expose.ts +0 -36
- package/layers/admin/app/utils/editor-form-context.ts +0 -82
- package/layers/admin/app/utils/editor-registry.ts +0 -20
- package/layers/admin/app/utils/field-keys.ts +0 -11
- package/layers/admin/app/utils/list-cell.ts +0 -46
- package/layers/admin/app/utils/list-columns.ts +0 -142
- package/layers/admin/app/utils/list-query.ts +0 -96
- package/layers/admin/app/utils/nav-active.ts +0 -9
- package/layers/admin/app/utils/populate-blocks.ts +0 -288
- package/layers/admin/app/utils/preview-channel.ts +0 -62
- package/layers/admin/app/utils/preview-viewport.ts +0 -80
- package/layers/admin/app/utils/reauth.ts +0 -67
- package/layers/admin/app/utils/record-title.ts +0 -15
- package/layers/admin/app/utils/register-builtin-editors.ts +0 -9
- package/layers/admin/app/utils/safe-redirect.ts +0 -12
- package/layers/admin/nuxt.config.ts +0 -20
- package/layers/auth/nuxt.config.ts +0 -1
- package/layers/auth/server/plugins/01.register-auth-pipelines.ts +0 -7
- package/layers/collections/nuxt.config.ts +0 -1
- package/layers/collections/server/plugins/01.register.ts +0 -29
- package/layers/collections/server/plugins/02.register-relation-populate.ts +0 -18
- package/layers/core/app/composables/layouts.ts +0 -5
- package/layers/core/app/utils/field-factories.ts +0 -129
- package/layers/core/app/utils/layouts.ts +0 -38
- package/layers/core/modules/auto-discovery/extract-block.ts +0 -179
- package/layers/core/modules/auto-discovery/index.ts +0 -112
- package/layers/core/modules/auto-discovery/package-registry.ts +0 -19
- package/layers/core/modules/auto-discovery/scan.ts +0 -170
- package/layers/core/modules/auto-discovery/virtual.d.ts +0 -20
- package/layers/core/modules/component-namespace/index.ts +0 -44
- package/layers/core/modules/component-namespace/shared.ts +0 -24
- package/layers/core/modules/kestrel/app-shell.ts +0 -55
- package/layers/core/modules/kestrel/auto-imports.ts +0 -44
- package/layers/core/modules/kestrel/index.ts +0 -144
- package/layers/core/modules/plugin-order/index.ts +0 -69
- package/layers/core/modules/plugin-order/plugin-order.ts +0 -273
- package/layers/core/nuxt.config.ts +0 -10
- package/layers/core/server/api/[...path].ts +0 -98
- package/layers/core/server/plugins/00.config.ts +0 -12
- package/layers/core/server/plugins/00.migrate.ts +0 -12
- package/layers/core/server/plugins/01.register-introspection-pipeline.ts +0 -5
- package/layers/core/server/plugins/01.register-openapi-pipeline.ts +0 -5
- package/layers/core/server/plugins/01.register-tooling-pipelines.ts +0 -5
- package/layers/core/server/plugins/02.schema-sync.ts +0 -58
- package/layers/core/server/plugins/04.outbox-worker.ts +0 -31
- package/layers/core/server/plugins/05.reindex-refs.ts +0 -5
- package/layers/core/server/routes/__kestrel/dashboard.get.ts +0 -29
- package/layers/core/server/tasks/db/migrate-module.ts +0 -83
- package/layers/core/server/tasks/db/migrate-revisions.ts +0 -20
- package/layers/core/server/tasks/db/migrate.ts +0 -50
- package/layers/core/server/utils/server-config.ts +0 -81
- package/layers/fields/nuxt.config.ts +0 -1
- package/layers/fields/server/plugins/01.register-blocks-pipeline.ts +0 -7
- package/layers/fields/server/plugins/01.register-field-populate.ts +0 -11
- package/layers/media/app/components/KestrelImg.vue +0 -94
- package/layers/media/app/components/MediaConflictDialog.vue +0 -35
- package/layers/media/app/components/MediaDeleteDialog.vue +0 -52
- package/layers/media/app/components/MediaGrid.vue +0 -113
- package/layers/media/app/components/MediaImage.vue +0 -70
- package/layers/media/app/components/MediaLibrary.vue +0 -456
- package/layers/media/app/components/MediaNewFolderDialog.vue +0 -32
- package/layers/media/app/components/MediaPathBar.vue +0 -163
- package/layers/media/app/components/MediaPicker.vue +0 -19
- package/layers/media/app/components/MediaRenameDialog.vue +0 -32
- package/layers/media/app/components/MediaTable.vue +0 -75
- package/layers/media/app/components/MediaThumb.vue +0 -25
- package/layers/media/app/components/MediaToolbar.vue +0 -90
- package/layers/media/app/components/MediaUploadDialog.vue +0 -87
- package/layers/media/app/components/MediaViewer.vue +0 -137
- package/layers/media/app/components/field/Media.vue +0 -263
- package/layers/media/app/composables/useBlurUp.ts +0 -55
- package/layers/media/app/composables/useMediaClipboard.ts +0 -14
- package/layers/media/app/composables/useMediaContextMenu.ts +0 -56
- package/layers/media/app/composables/useMediaDnd.ts +0 -56
- package/layers/media/app/composables/useMediaLibrary.ts +0 -152
- package/layers/media/app/composables/useMediaOps.ts +0 -50
- package/layers/media/app/composables/useMediaRelocate.ts +0 -39
- package/layers/media/app/composables/useMediaResolver.ts +0 -50
- package/layers/media/app/composables/useMediaUpload.ts +0 -93
- package/layers/media/app/composables/useMediaVariant.ts +0 -34
- package/layers/media/app/plugins/field-media.client.ts +0 -6
- package/layers/media/app/utils/ai-disclosure.ts +0 -19
- package/layers/media/app/utils/dnd.ts +0 -69
- package/layers/media/app/utils/library.ts +0 -81
- package/layers/media/app/utils/ops.ts +0 -60
- package/layers/media/app/utils/picture-model.ts +0 -125
- package/layers/media/app/utils/thumbhash.ts +0 -19
- package/layers/media/app/utils/variant-name.ts +0 -20
- package/layers/media/nuxt.config.ts +0 -7
- package/layers/media/server/middleware/ondemand-variants.ts +0 -38
- package/layers/media/server/plugins/01.register-media-pipelines.ts +0 -7
- package/layers/media/server/plugins/02.register-media.ts +0 -22
- package/layers/media/server/plugins/04.variant-capture.ts +0 -27
- package/layers/media/server/plugins/05.media-cleanup.ts +0 -5
- package/layers/media/server/tasks/media/backfill.ts +0 -17
- package/layers/public/app/app.vue +0 -5
- package/layers/public/app/components/BlockRenderer.vue +0 -56
- package/layers/public/app/components/KestrelLink.vue +0 -26
- package/layers/public/app/components/KestrelPreviewBridge.vue +0 -133
- package/layers/public/app/composables/public-page.ts +0 -24
- package/layers/public/app/error.vue +0 -12
- package/layers/public/app/layouts/default.vue +0 -5
- package/layers/public/app/pages/[...slug].vue +0 -239
- package/layers/public/app/pages/__kestrel/preview.vue +0 -46
- package/layers/public/app/utils/block-edit-context.ts +0 -16
- package/layers/public/app/utils/json-ld.ts +0 -139
- package/layers/public/app/utils/link-href.ts +0 -46
- package/layers/public/app/utils/page-head.ts +0 -84
- package/layers/public/app/utils/page-layout.ts +0 -18
- package/layers/public/app/utils/preview-protocol.ts +0 -117
- package/layers/public/app/utils/public-route.ts +0 -46
- package/layers/public/app/utils/site-head.ts +0 -43
- package/layers/public/modules/deploy-output/index.ts +0 -80
- package/layers/public/modules/prerender-routes/discover.ts +0 -37
- package/layers/public/modules/prerender-routes/index.ts +0 -88
- package/layers/public/modules/prerender-routes/routes.ts +0 -16
- package/layers/public/modules/prune-media/index.ts +0 -48
- package/layers/public/modules/prune-media/prune-media.ts +0 -81
- package/layers/public/nuxt.config.ts +0 -14
- package/layers/public/server/delivery-live/serve.ts +0 -111
- package/layers/public/server/middleware/delivery-live-catchall.ts +0 -7
- package/layers/public/server/pipelines/index.ts +0 -23
- package/layers/public/server/plugins/00.ensure-snapshot-triggers.ts +0 -14
- package/layers/public/server/plugins/01.register-public-pipelines.ts +0 -7
- package/layers/public/server/plugins/02.register-links.ts +0 -16
- package/layers/public/server/plugins/03.redirects.ts +0 -55
- package/layers/public/server/plugins/05.plan-publish.ts +0 -5
- package/layers/public/server/plugins/zz.publish.ts +0 -127
- package/layers/public/server/routes/llms-full.txt.get.ts +0 -99
- package/layers/public/server/routes/llms.txt.get.ts +0 -66
- package/layers/public/server/routes/redirects.json.get.ts +0 -58
- package/layers/public/server/routes/robots.txt.get.ts +0 -11
- package/layers/public/server/routes/sitemap.xml.get.ts +0 -79
- package/layers/public/server/tasks/publish/run.ts +0 -32
- package/layers/public/server/utils/publish/render-live.ts +0 -26
- package/layers/ui/app/assets/scss/_base.scss +0 -16
- package/layers/ui/app/assets/scss/_datepicker.scss +0 -82
- package/layers/ui/app/assets/scss/_mixins.scss +0 -50
- package/layers/ui/app/assets/scss/_reset.scss +0 -65
- package/layers/ui/app/assets/scss/_tokens.scss +0 -183
- package/layers/ui/app/assets/scss/main.scss +0 -4
- package/layers/ui/app/components/field/Boolean.vue +0 -30
- package/layers/ui/app/components/field/Choice.vue +0 -73
- package/layers/ui/app/components/field/Datetime.vue +0 -98
- package/layers/ui/app/components/field/DeadRefNote.vue +0 -28
- package/layers/ui/app/components/field/Json.vue +0 -62
- package/layers/ui/app/components/field/Layout.vue +0 -138
- package/layers/ui/app/components/field/Link.vue +0 -112
- package/layers/ui/app/components/field/LinkInternalPicker.vue +0 -95
- package/layers/ui/app/components/field/Number.vue +0 -30
- package/layers/ui/app/components/field/Relation.vue +0 -49
- package/layers/ui/app/components/field/Renderer.vue +0 -30
- package/layers/ui/app/components/field/Repeater.vue +0 -460
- package/layers/ui/app/components/field/Richtext.vue +0 -26
- package/layers/ui/app/components/field/Slug.vue +0 -65
- package/layers/ui/app/components/field/Text.vue +0 -35
- package/layers/ui/app/components/field/Unsupported.vue +0 -34
- package/layers/ui/app/components/ui/Alert.vue +0 -50
- package/layers/ui/app/components/ui/Brand.vue +0 -22
- package/layers/ui/app/components/ui/Button.vue +0 -116
- package/layers/ui/app/components/ui/ButtonGroup.vue +0 -96
- package/layers/ui/app/components/ui/Checkbox.vue +0 -53
- package/layers/ui/app/components/ui/CheckboxGroup.vue +0 -51
- package/layers/ui/app/components/ui/Combobox.vue +0 -345
- package/layers/ui/app/components/ui/DatePicker.vue +0 -92
- package/layers/ui/app/components/ui/DateRangePicker.vue +0 -100
- package/layers/ui/app/components/ui/Dialog.vue +0 -107
- package/layers/ui/app/components/ui/EmptyState.vue +0 -46
- package/layers/ui/app/components/ui/Field.vue +0 -58
- package/layers/ui/app/components/ui/Fieldset.vue +0 -65
- package/layers/ui/app/components/ui/Icon.vue +0 -44
- package/layers/ui/app/components/ui/Menu.vue +0 -54
- package/layers/ui/app/components/ui/NumberInput.vue +0 -175
- package/layers/ui/app/components/ui/Richtext.vue +0 -141
- package/layers/ui/app/components/ui/RichtextToolbar.vue +0 -251
- package/layers/ui/app/components/ui/Select.vue +0 -52
- package/layers/ui/app/components/ui/TextInput.vue +0 -111
- package/layers/ui/app/components/ui/Textarea.vue +0 -39
- package/layers/ui/app/components/ui/TimeInput.vue +0 -18
- package/layers/ui/app/components/ui/Toasts.vue +0 -103
- package/layers/ui/app/components/ui/Tooltip.vue +0 -45
- package/layers/ui/app/composables/useAdminLang.ts +0 -8
- package/layers/ui/app/composables/useDragReorder.ts +0 -41
- package/layers/ui/app/composables/useEchoGuard.ts +0 -28
- package/layers/ui/app/composables/useLinkField.ts +0 -70
- package/layers/ui/app/composables/useRecordOptions.ts +0 -84
- package/layers/ui/app/composables/useRepeater.ts +0 -99
- package/layers/ui/app/composables/useT.ts +0 -32
- package/layers/ui/app/composables/useToast.ts +0 -46
- package/layers/ui/app/i18n/de.ts +0 -432
- package/layers/ui/app/i18n/en.ts +0 -434
- package/layers/ui/app/utils/boolean-field.ts +0 -10
- package/layers/ui/app/utils/field-component.ts +0 -22
- package/layers/ui/app/utils/field-empty.ts +0 -21
- package/layers/ui/app/utils/field-registry.ts +0 -36
- package/layers/ui/app/utils/humanize.ts +0 -14
- package/layers/ui/app/utils/icons.ts +0 -143
- package/layers/ui/app/utils/iso-date.ts +0 -18
- package/layers/ui/app/utils/localized.ts +0 -13
- package/layers/ui/app/utils/record-context.ts +0 -15
- package/layers/ui/app/utils/reorder.ts +0 -8
- package/layers/ui/app/utils/richtext-preserve-class.ts +0 -31
- package/layers/ui/app/utils/useFieldA11y.ts +0 -40
- package/layers/ui/nuxt.config.ts +0 -9
- package/nuxt.config.ts +0 -108
- package/scripts/consumer-template-ci.mjs +0 -160
- package/scripts/copy-create-payload.mjs +0 -49
- package/scripts/coverage-all.mjs +0 -163
- package/scripts/dashboard.mjs +0 -89
- package/scripts/hash-password.mjs +0 -29
- package/scripts/kestrel.mjs +0 -240
- package/scripts/lib/cli.mjs +0 -129
- package/scripts/lib/coverage-model.mjs +0 -51
- package/scripts/lib/password.mjs +0 -19
- package/scripts/lib/scaffold.mjs +0 -205
- package/scripts/port-graph.mjs +0 -350
- package/scripts/typecheck.mjs +0 -62
- package/templates/starter/README.md +0 -65
- package/templates/starter/_env.example +0 -17
- package/templates/starter/_gitignore +0 -26
- package/templates/starter/_package.json +0 -22
- package/templates/starter/app/app.vue +0 -7
- package/templates/starter/app/blocks/Prose.vue +0 -13
- package/templates/starter/app/layouts/default.vue +0 -6
- package/templates/starter/nuxt.config.ts +0 -23
- package/templates/starter/pnpm-workspace.yaml +0 -8
- 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,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>
|