@michaelthielemann/kestrel 4.1.3 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,70 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { PER_PAGE_OPTIONS } from '@michaelthielemann/kestrel-core/client'
|
|
3
|
-
|
|
4
|
-
const props = defineProps<{ page: number; totalPages: number; total: number; perPage: number }>()
|
|
5
|
-
const emit = defineEmits<{ 'update:page': [page: number]; 'update:perPage': [perPage: number] }>()
|
|
6
|
-
|
|
7
|
-
const { t } = useT()
|
|
8
|
-
// The per-page `<select>` is hand-rolled here (no UiField wrapper), so the label↔control association needs
|
|
9
|
-
// an explicit id pair for static a11y analysis to see it.
|
|
10
|
-
const perPageId = useId()
|
|
11
|
-
|
|
12
|
-
function prev() {
|
|
13
|
-
if (props.page > 1) emit('update:page', props.page - 1)
|
|
14
|
-
}
|
|
15
|
-
function next() {
|
|
16
|
-
if (props.page < props.totalPages) emit('update:page', props.page + 1)
|
|
17
|
-
}
|
|
18
|
-
</script>
|
|
19
|
-
|
|
20
|
-
<template>
|
|
21
|
-
<div class="list__pager">
|
|
22
|
-
<KestrelUiButton type="button" size="sm" :disabled="page <= 1" @click="prev">{{ t('list.prev') }}</KestrelUiButton>
|
|
23
|
-
<span class="list__page">{{ t('list.page', { page, totalPages, total }) }}</span>
|
|
24
|
-
<KestrelUiButton type="button" size="sm" :disabled="page >= totalPages" @click="next">{{ t('list.next') }}</KestrelUiButton>
|
|
25
|
-
<label class="list__perpage" :for="perPageId">
|
|
26
|
-
<span class="list__perpage-label">{{ t('list.perPage') }}</span>
|
|
27
|
-
<select
|
|
28
|
-
:id="perPageId"
|
|
29
|
-
class="list__perpage-select"
|
|
30
|
-
:value="perPage"
|
|
31
|
-
:aria-label="t('list.perPage')"
|
|
32
|
-
@change="emit('update:perPage', Number(($event.target as HTMLSelectElement).value))"
|
|
33
|
-
>
|
|
34
|
-
<option v-for="n in PER_PAGE_OPTIONS" :key="n" :value="n">{{ n }}</option>
|
|
35
|
-
</select>
|
|
36
|
-
</label>
|
|
37
|
-
</div>
|
|
38
|
-
</template>
|
|
39
|
-
|
|
40
|
-
<style lang="scss">
|
|
41
|
-
.list {
|
|
42
|
-
&__pager {
|
|
43
|
-
display: flex;
|
|
44
|
-
align-items: center;
|
|
45
|
-
gap: var(--space-3);
|
|
46
|
-
}
|
|
47
|
-
&__page {
|
|
48
|
-
font-size: var(--text-sm);
|
|
49
|
-
color: var(--color-text-muted);
|
|
50
|
-
}
|
|
51
|
-
// Per-page size selector sits at the far end of the pager.
|
|
52
|
-
&__perpage {
|
|
53
|
-
display: inline-flex;
|
|
54
|
-
align-items: center;
|
|
55
|
-
gap: var(--space-2);
|
|
56
|
-
margin-inline-start: auto;
|
|
57
|
-
font-size: var(--text-sm);
|
|
58
|
-
color: var(--color-text-muted);
|
|
59
|
-
}
|
|
60
|
-
&__perpage-select {
|
|
61
|
-
padding: var(--space-1) var(--space-2);
|
|
62
|
-
border: 1px solid var(--color-border);
|
|
63
|
-
border-radius: var(--radius-sm);
|
|
64
|
-
background: var(--color-surface);
|
|
65
|
-
color: var(--color-text);
|
|
66
|
-
font: inherit;
|
|
67
|
-
font-size: var(--text-sm);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
</style>
|
|
@@ -1,347 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { sortDirection } from '../utils/list-query'
|
|
3
|
-
import { cellDisplay, columnLabel, rowLabel as labelForRow } from '../utils/list-cell'
|
|
4
|
-
import type { ListColumn } from '../utils/list-columns'
|
|
5
|
-
import { resolveLocalized } from '../../../ui/app/utils/localized'
|
|
6
|
-
import type { SerializedAction } from '@michaelthielemann/kestrel-core'
|
|
7
|
-
|
|
8
|
-
const props = defineProps<{
|
|
9
|
-
rows: Record<string, unknown>[]
|
|
10
|
-
columns: ListColumn[]
|
|
11
|
-
collection: string
|
|
12
|
-
localeQuery: string
|
|
13
|
-
sort: string
|
|
14
|
-
busy: boolean
|
|
15
|
-
selected: Set<number>
|
|
16
|
-
allSelected: boolean
|
|
17
|
-
headerIndeterminate: boolean
|
|
18
|
-
/** Schema-driven row actions beyond the built-in edit/duplicate/delete. */
|
|
19
|
-
actions?: SerializedAction[]
|
|
20
|
-
}>()
|
|
21
|
-
const emit = defineEmits<{
|
|
22
|
-
sort: [key: string]
|
|
23
|
-
toggleRow: [id: number, on: boolean]
|
|
24
|
-
toggleAll: [on: boolean]
|
|
25
|
-
duplicate: [id: number]
|
|
26
|
-
delete: [ids: number[]]
|
|
27
|
-
runAction: [action: SerializedAction, id: number]
|
|
28
|
-
}>()
|
|
29
|
-
|
|
30
|
-
const { t, lang } = useT()
|
|
31
|
-
const { locales } = useContentLocales()
|
|
32
|
-
|
|
33
|
-
const colLabel = (col: ListColumn) => columnLabel(col, t)
|
|
34
|
-
const rowLabel = (row: Record<string, unknown>) => labelForRow(props.columns, row)
|
|
35
|
-
const actionLabel = (action: SerializedAction) => resolveLocalized(action.label, lang.value) ?? action.name
|
|
36
|
-
|
|
37
|
-
function arrow(field: string) {
|
|
38
|
-
const dir = sortDirection(props.sort, field)
|
|
39
|
-
return dir === 'asc' ? ' ▲' : dir === 'desc' ? ' ▼' : ''
|
|
40
|
-
}
|
|
41
|
-
// Programmatic sort state for AT (the visual arrow is aria-hidden) — WCAG 1.3.1 / 4.1.2.
|
|
42
|
-
function ariaSort(field: string): 'ascending' | 'descending' | 'none' {
|
|
43
|
-
const dir = sortDirection(props.sort, field)
|
|
44
|
-
return dir === 'asc' ? 'ascending' : dir === 'desc' ? 'descending' : 'none'
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Per-row translation status (the list query attaches `$translations`: locale → sibling id, or null).
|
|
48
|
-
// Present locales link to their sibling; missing ones offer create-and-link, carrying the translation group.
|
|
49
|
-
function rowTranslations(row: Record<string, unknown>) {
|
|
50
|
-
const map = (row.$translations ?? {}) as Record<string, number | null>
|
|
51
|
-
const group = row.translationGroup as string | undefined
|
|
52
|
-
return locales.map((loc) => {
|
|
53
|
-
const id = map[loc]
|
|
54
|
-
return id != null
|
|
55
|
-
? { loc, present: true, to: `/admin/${props.collection}/${id}?locale=${loc}` }
|
|
56
|
-
: { loc, present: false, to: `/admin/${props.collection}/new?locale=${loc}${group ? `&group=${group}` : ''}` }
|
|
57
|
-
})
|
|
58
|
-
}
|
|
59
|
-
</script>
|
|
60
|
-
|
|
61
|
-
<template>
|
|
62
|
-
<table class="list__table">
|
|
63
|
-
<thead>
|
|
64
|
-
<tr>
|
|
65
|
-
<th class="list__select-th" scope="col">
|
|
66
|
-
<KestrelUiCheckbox
|
|
67
|
-
:model-value="allSelected"
|
|
68
|
-
:indeterminate="headerIndeterminate"
|
|
69
|
-
:aria-label="t('list.selectAll')"
|
|
70
|
-
@update:model-value="(v) => emit('toggleAll', v)"
|
|
71
|
-
/>
|
|
72
|
-
</th>
|
|
73
|
-
<!-- Row actions: an always-visible, left-frozen chrome column placed right after Select. Its
|
|
74
|
-
accessible name is real visually-hidden text, not aria-label on a static <th> (which several
|
|
75
|
-
screen-reader / browser pairs ignore on non-interactive table cells). -->
|
|
76
|
-
<th class="list__actions-th" scope="col">
|
|
77
|
-
<span class="list__vh">{{ t('a11y.rowActions') }}</span>
|
|
78
|
-
</th>
|
|
79
|
-
<th
|
|
80
|
-
v-for="c in columns"
|
|
81
|
-
:key="c.key"
|
|
82
|
-
scope="col"
|
|
83
|
-
:aria-sort="c.sortable ? ariaSort(c.key) : undefined"
|
|
84
|
-
:class="{ 'list__th-narrow': c.type === 'translations' || c.type === 'deadRefs' }"
|
|
85
|
-
>
|
|
86
|
-
<button v-if="c.sortable" type="button" class="list__sort" @click="emit('sort', c.key)">{{ colLabel(c) }}<span aria-hidden="true">{{ arrow(c.key) }}</span></button>
|
|
87
|
-
<span v-else>{{ colLabel(c) }}</span>
|
|
88
|
-
</th>
|
|
89
|
-
</tr>
|
|
90
|
-
</thead>
|
|
91
|
-
<tbody>
|
|
92
|
-
<tr v-for="row in rows" :key="String(row.id)" class="list__row">
|
|
93
|
-
<td class="list__select-cell">
|
|
94
|
-
<KestrelUiCheckbox
|
|
95
|
-
:model-value="selected.has(Number(row.id))"
|
|
96
|
-
:aria-label="t('list.selectRow', { name: rowLabel(row) })"
|
|
97
|
-
@update:model-value="(v) => emit('toggleRow', Number(row.id), v)"
|
|
98
|
-
/>
|
|
99
|
-
</td>
|
|
100
|
-
<td class="list__actions-cell">
|
|
101
|
-
<div class="list__row-actions">
|
|
102
|
-
<NuxtLink :to="`/admin/${collection}/${row.id}${localeQuery}`" class="list__action-btn" :aria-label="t('list.rowEdit', { name: rowLabel(row) })">
|
|
103
|
-
<KestrelUiIcon name="pencil" :size="15" />
|
|
104
|
-
</NuxtLink>
|
|
105
|
-
<button type="button" class="list__action-btn" :disabled="busy" :aria-label="t('list.rowDuplicate', { name: rowLabel(row) })" @click="emit('duplicate', Number(row.id))">
|
|
106
|
-
<KestrelUiIcon name="copy" :size="15" />
|
|
107
|
-
</button>
|
|
108
|
-
<button
|
|
109
|
-
v-for="action in actions"
|
|
110
|
-
:key="action.name"
|
|
111
|
-
type="button"
|
|
112
|
-
class="list__action-btn"
|
|
113
|
-
:disabled="busy"
|
|
114
|
-
:data-action="action.name"
|
|
115
|
-
:aria-label="`${actionLabel(action)}: ${rowLabel(row)}`"
|
|
116
|
-
@click="emit('runAction', action, Number(row.id))"
|
|
117
|
-
>
|
|
118
|
-
<KestrelUiIcon :name="action.icon ?? 'zap'" :size="15" />
|
|
119
|
-
</button>
|
|
120
|
-
<button type="button" class="list__action-btn list__action-btn--danger" :disabled="busy" :aria-label="t('list.rowDelete', { name: rowLabel(row) })" @click="emit('delete', [Number(row.id)])">
|
|
121
|
-
<KestrelUiIcon name="trash" :size="15" />
|
|
122
|
-
</button>
|
|
123
|
-
</div>
|
|
124
|
-
</td>
|
|
125
|
-
<!-- A quarantined row carries none of its normal fields — a single spanning cell replaces
|
|
126
|
-
the per-column cells rather than rendering blanks for data that doesn't exist. -->
|
|
127
|
-
<td v-if="row.$quarantined === true" :colspan="columns.length">
|
|
128
|
-
<span class="list__quarantine-badge">
|
|
129
|
-
<KestrelUiIcon name="triangle-alert" :size="14" role="img" :aria-label="t('list.quarantined')" />
|
|
130
|
-
{{ t('list.quarantined') }}
|
|
131
|
-
</span>
|
|
132
|
-
</td>
|
|
133
|
-
<td v-else v-for="c in columns" :key="c.key" :class="{ 'list__narrow-cell': c.type === 'translations' || c.type === 'deadRefs' }">
|
|
134
|
-
<span v-if="c.type === 'deadRefs'" class="list__deadrefs">
|
|
135
|
-
<KestrelUiIcon
|
|
136
|
-
v-if="row.$hasDeadRefs"
|
|
137
|
-
name="triangle-alert"
|
|
138
|
-
:size="15"
|
|
139
|
-
class="list__deadref-icon"
|
|
140
|
-
role="img"
|
|
141
|
-
:aria-label="t('deadRefs.listWarning')"
|
|
142
|
-
:title="t('deadRefs.listWarning')"
|
|
143
|
-
/>
|
|
144
|
-
</span>
|
|
145
|
-
<span v-else-if="c.type === 'translations'" class="list__transl">
|
|
146
|
-
<NuxtLink
|
|
147
|
-
v-for="tr in rowTranslations(row)"
|
|
148
|
-
:key="tr.loc"
|
|
149
|
-
:to="tr.to"
|
|
150
|
-
:data-loc="tr.loc"
|
|
151
|
-
class="list__badge"
|
|
152
|
-
:class="tr.present ? 'list__badge--present' : 'list__badge--missing'"
|
|
153
|
-
:title="tr.present ? t('list.editLocale', { loc: tr.loc.toUpperCase() }) : t('list.createLocale', { loc: tr.loc.toUpperCase() })"
|
|
154
|
-
:aria-label="tr.present ? t('list.editLocale', { loc: tr.loc.toUpperCase() }) : t('list.createLocale', { loc: tr.loc.toUpperCase() })"
|
|
155
|
-
>{{ tr.loc.toUpperCase() }}</NuxtLink>
|
|
156
|
-
</span>
|
|
157
|
-
<template v-else>{{ cellDisplay(c, row) }}</template>
|
|
158
|
-
</td>
|
|
159
|
-
</tr>
|
|
160
|
-
</tbody>
|
|
161
|
-
</table>
|
|
162
|
-
</template>
|
|
163
|
-
|
|
164
|
-
<style lang="scss">
|
|
165
|
-
// Visually-hidden text for a static cell that must be announced but not shown (the Actions column header).
|
|
166
|
-
// Chosen over aria-label, which is unreliably announced on non-interactive table cells.
|
|
167
|
-
.list__vh {
|
|
168
|
-
position: absolute;
|
|
169
|
-
width: 1px;
|
|
170
|
-
height: 1px;
|
|
171
|
-
padding: 0;
|
|
172
|
-
margin: -1px;
|
|
173
|
-
overflow: hidden;
|
|
174
|
-
clip: rect(0, 0, 0, 0);
|
|
175
|
-
white-space: nowrap;
|
|
176
|
-
border: 0;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
.list {
|
|
180
|
-
&__table {
|
|
181
|
-
width: 100%;
|
|
182
|
-
border-collapse: collapse;
|
|
183
|
-
font-size: var(--text-sm);
|
|
184
|
-
|
|
185
|
-
th,
|
|
186
|
-
td {
|
|
187
|
-
text-align: start;
|
|
188
|
-
padding: var(--space-2) var(--space-3);
|
|
189
|
-
border-bottom: 1px solid var(--color-border);
|
|
190
|
-
vertical-align: top;
|
|
191
|
-
}
|
|
192
|
-
// Keep the column headers visible while the rows scroll inside .list__scroll. A solid background is
|
|
193
|
-
// required so scrolling rows don't show through. Part of the frozen-column z-index stack (see the
|
|
194
|
-
// &__select-* / &__actions-* rules): scrolling body td 0 < frozen body td 1 < scrolling header th 2 <
|
|
195
|
-
// frozen corner th 3, so nothing bleeds through on either scroll axis.
|
|
196
|
-
thead th {
|
|
197
|
-
position: sticky;
|
|
198
|
-
top: 0;
|
|
199
|
-
z-index: 2; // scrolling header: above scrolling body cells (0) and the frozen body columns (1)
|
|
200
|
-
background: var(--color-bg);
|
|
201
|
-
}
|
|
202
|
-
// Frozen corner: the select + actions headers are sticky on BOTH axes, so they must sit above every
|
|
203
|
-
// scrolling header AND every frozen body column where the two frozen edges cross.
|
|
204
|
-
thead th.list__select-th,
|
|
205
|
-
thead th.list__actions-th {
|
|
206
|
-
z-index: 3;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
&__sort {
|
|
210
|
-
background: none;
|
|
211
|
-
border: 0;
|
|
212
|
-
padding: 0;
|
|
213
|
-
font: inherit;
|
|
214
|
-
font-weight: var(--weight-medium);
|
|
215
|
-
cursor: pointer;
|
|
216
|
-
color: var(--color-text);
|
|
217
|
-
}
|
|
218
|
-
// Compact, de-emphasised info columns (translations · dead-reference warning).
|
|
219
|
-
&__th-narrow,
|
|
220
|
-
&__narrow-cell {
|
|
221
|
-
width: 1%;
|
|
222
|
-
white-space: nowrap;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Quarantine badge — icon + text (not color alone, WCAG 1.4.1), replacing a row's normal data cells.
|
|
226
|
-
&__quarantine-badge {
|
|
227
|
-
display: inline-flex;
|
|
228
|
-
align-items: center;
|
|
229
|
-
gap: var(--space-1);
|
|
230
|
-
padding: 2px var(--space-2);
|
|
231
|
-
border: 1px solid var(--color-danger);
|
|
232
|
-
border-radius: var(--radius-sm);
|
|
233
|
-
color: var(--color-danger);
|
|
234
|
-
font-size: var(--text-xs);
|
|
235
|
-
font-weight: var(--weight-medium);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Dead-reference warning: an amber triangle, shown only on rows holding a stale reference.
|
|
239
|
-
&__deadrefs {
|
|
240
|
-
display: inline-flex;
|
|
241
|
-
}
|
|
242
|
-
&__deadref-icon {
|
|
243
|
-
color: var(--color-warning);
|
|
244
|
-
}
|
|
245
|
-
&__transl {
|
|
246
|
-
display: inline-flex;
|
|
247
|
-
gap: 2px;
|
|
248
|
-
flex-wrap: wrap;
|
|
249
|
-
}
|
|
250
|
-
&__badge {
|
|
251
|
-
display: inline-flex;
|
|
252
|
-
align-items: center;
|
|
253
|
-
justify-content: center;
|
|
254
|
-
min-width: 1.4rem;
|
|
255
|
-
padding: 0 3px;
|
|
256
|
-
border: 1px solid transparent;
|
|
257
|
-
border-radius: var(--radius-sm);
|
|
258
|
-
font-size: 0.625rem;
|
|
259
|
-
font-weight: var(--weight-medium);
|
|
260
|
-
line-height: 1.5;
|
|
261
|
-
letter-spacing: 0.02em;
|
|
262
|
-
text-decoration: none;
|
|
263
|
-
|
|
264
|
-
&--present {
|
|
265
|
-
background: var(--color-primary-soft);
|
|
266
|
-
color: var(--color-primary);
|
|
267
|
-
}
|
|
268
|
-
&--missing {
|
|
269
|
-
color: var(--color-text-muted);
|
|
270
|
-
border-color: var(--color-border);
|
|
271
|
-
border-style: dashed;
|
|
272
|
-
}
|
|
273
|
-
&:focus-visible {
|
|
274
|
-
outline: 2px solid var(--color-focus);
|
|
275
|
-
outline-offset: -2px;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Selection column — the first frozen chrome column, pinned to the left with `position: sticky` inside
|
|
280
|
-
// the existing .list__scroll (no new scroll container). Opaque `var(--color-bg)` so horizontally-scrolled
|
|
281
|
-
// data can't bleed through; z-index 1 lifts the frozen body cell above the scrolling body cells.
|
|
282
|
-
&__select-th,
|
|
283
|
-
&__select-cell {
|
|
284
|
-
position: sticky;
|
|
285
|
-
left: 0;
|
|
286
|
-
z-index: 1;
|
|
287
|
-
width: var(--list-col-select);
|
|
288
|
-
white-space: nowrap;
|
|
289
|
-
vertical-align: middle;
|
|
290
|
-
background: var(--color-bg);
|
|
291
|
-
}
|
|
292
|
-
// Row-actions column — the second frozen column, sitting immediately after Select (its `left` offset is
|
|
293
|
-
// exactly the select column's width). Always visible; snug `width: 1%` so the three icon buttons never
|
|
294
|
-
// steal width from the Title column.
|
|
295
|
-
&__actions-th,
|
|
296
|
-
&__actions-cell {
|
|
297
|
-
position: sticky;
|
|
298
|
-
left: var(--list-col-select);
|
|
299
|
-
z-index: 1;
|
|
300
|
-
width: 1%;
|
|
301
|
-
white-space: nowrap;
|
|
302
|
-
vertical-align: middle;
|
|
303
|
-
background: var(--color-bg);
|
|
304
|
-
}
|
|
305
|
-
// A plain, always-visible inline row of icon buttons (Edit → Duplicate → Delete; destructive last, never
|
|
306
|
-
// default focus). BlockTree.vue deliberately keeps its own absolute overlay: a tree row is a single flex
|
|
307
|
-
// row with no column grid — there is no cell to place actions in.
|
|
308
|
-
&__row-actions {
|
|
309
|
-
display: inline-flex;
|
|
310
|
-
align-items: center;
|
|
311
|
-
gap: 1px;
|
|
312
|
-
}
|
|
313
|
-
&__action-btn {
|
|
314
|
-
display: flex;
|
|
315
|
-
align-items: center;
|
|
316
|
-
justify-content: center;
|
|
317
|
-
width: 1.5rem;
|
|
318
|
-
height: 1.5rem;
|
|
319
|
-
border: 0;
|
|
320
|
-
border-radius: var(--radius-sm);
|
|
321
|
-
background: transparent;
|
|
322
|
-
color: var(--color-text-muted); /* meaningful icon control at rest → ≥3:1 (SC 1.4.11) */
|
|
323
|
-
text-decoration: none;
|
|
324
|
-
cursor: pointer;
|
|
325
|
-
transition:
|
|
326
|
-
background-color var(--motion-fast) var(--ease-standard),
|
|
327
|
-
color var(--motion-fast) var(--ease-standard);
|
|
328
|
-
|
|
329
|
-
&:hover {
|
|
330
|
-
background: var(--color-hover);
|
|
331
|
-
color: var(--color-text);
|
|
332
|
-
}
|
|
333
|
-
&:focus-visible {
|
|
334
|
-
outline: 2px solid var(--color-focus);
|
|
335
|
-
outline-offset: -2px;
|
|
336
|
-
}
|
|
337
|
-
&--danger:hover {
|
|
338
|
-
color: var(--color-danger);
|
|
339
|
-
}
|
|
340
|
-
&:disabled {
|
|
341
|
-
opacity: 0.5;
|
|
342
|
-
cursor: default;
|
|
343
|
-
pointer-events: none;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
</style>
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
// The editor "Ampel": TWO lamps side by side in the action bar, each a dot + ONE word + a details tooltip.
|
|
3
|
-
// LEFT — the SAVE / draft lifecycle of THIS record (its local vs. stored state):
|
|
4
|
-
// amber — work in flight: saving, or unsaved local changes not yet in the DB
|
|
5
|
-
// blue — saved but a Draft, so it is intentionally NOT on the live/static site
|
|
6
|
-
// green — saved and (for status collections) published, or just saved (no status field)
|
|
7
|
-
// RIGHT — the GENERATED state of this record's static page (pageLike collections only), read from
|
|
8
|
-
// `publish_status` via `/api/publishStatus`:
|
|
9
|
-
// green — the route's last publish succeeded (the file is live; the tooltip names local vs S3)
|
|
10
|
-
// amber — published but no success row yet — a republish is in flight ("Generating…")
|
|
11
|
-
// neutral — this environment never produces the file (dev / static output off) → "Not built"
|
|
12
|
-
// red — the last publish for the route errored (render / write / S3) — details in the tooltip
|
|
13
|
-
// blue — a Draft: intentionally not generated, so there is no live file
|
|
14
|
-
import type { PublishStatusData } from '../composables/usePublishStatus'
|
|
15
|
-
|
|
16
|
-
const props = defineProps<{
|
|
17
|
-
dirty: boolean
|
|
18
|
-
saving?: boolean
|
|
19
|
-
hasStatus?: boolean
|
|
20
|
-
/** The LIVE (possibly unsaved) form status — drives the left lamp's word. */
|
|
21
|
-
status?: string
|
|
22
|
-
/** The last SAVED publish status — drives the RIGHT lamp's draft/published decision, so an unsaved
|
|
23
|
-
* dropdown switch never flips the generated-state indicator. Falls back to `status` when absent. */
|
|
24
|
-
savedStatus?: string
|
|
25
|
-
/** Only pageLike collections produce a static page → only they get the right (live) lamp. */
|
|
26
|
-
pageLike?: boolean
|
|
27
|
-
/** The record's last publish outcome (from `/api/publishStatus`); null/absent while unknown. */
|
|
28
|
-
live?: PublishStatusData | null
|
|
29
|
-
}>()
|
|
30
|
-
const { t, lang } = useT()
|
|
31
|
-
|
|
32
|
-
// LEFT lamp — save + publish state. `word` is the single label shown; `detail` is the tooltip sentence.
|
|
33
|
-
const save = computed<{ tone: 'amber' | 'green' | 'blue'; word: string; detail: string }>(() => {
|
|
34
|
-
if (props.saving) return { tone: 'amber', word: t('editorStatus.word.saving'), detail: t('editorStatus.detail.saving') }
|
|
35
|
-
if (props.dirty) return { tone: 'amber', word: t('editorStatus.word.unsaved'), detail: t('editorStatus.detail.unsaved') }
|
|
36
|
-
if (props.hasStatus && props.status === 'draft') return { tone: 'blue', word: t('editorStatus.word.draft'), detail: t('editorStatus.detail.draft') }
|
|
37
|
-
if (props.hasStatus) return { tone: 'green', word: t('editorStatus.word.published'), detail: t('editorStatus.detail.published') }
|
|
38
|
-
return { tone: 'green', word: t('editorStatus.word.saved'), detail: t('editorStatus.detail.saved') }
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
const updatedAtLabel = computed(() => {
|
|
42
|
-
const at = props.live?.updatedAt
|
|
43
|
-
if (!at) return ''
|
|
44
|
-
const d = new Date(at)
|
|
45
|
-
if (Number.isNaN(d.getTime())) return ''
|
|
46
|
-
return t('editorStatus.live.updatedAt', { time: d.toLocaleString(lang.value) })
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
// RIGHT lamp — the live / generated state of this record's static page. Only a SAVED pageLike record with a
|
|
50
|
-
// resolved route has one (a non-pageLike collection produces no static page; an unsaved `new` record leaves
|
|
51
|
-
// `live.route` null → no right lamp until the record actually has a page).
|
|
52
|
-
const live = computed<{ tone: 'green' | 'red' | 'amber' | 'blue' | 'neutral'; word: string; detail: string; when?: string; error?: string } | null>(() => {
|
|
53
|
-
if (props.pageLike !== true || !props.live?.route) return null
|
|
54
|
-
// Key the draft/published decision off the SAVED status, not the live form value: an unsaved dropdown
|
|
55
|
-
// switch must not move the generated-state indicator. Switching a live page → Draft keeps it "Live" (the
|
|
56
|
-
// file stays on the server until the save prunes it); switching a saved Draft → Published keeps it "Not
|
|
57
|
-
// live" (nothing is generated until the publish is actually saved). A DISTINCT word from the left lamp's
|
|
58
|
-
// "Draft" so the two lamps read as two signals (save-state vs generated-state), not a repeat.
|
|
59
|
-
const saved = props.savedStatus ?? props.status
|
|
60
|
-
if (props.hasStatus && saved === 'draft')
|
|
61
|
-
return { tone: 'blue', word: t('editorStatus.word.notLive'), detail: t('editorStatus.live.draft') }
|
|
62
|
-
const s = props.live?.status ?? null
|
|
63
|
-
if (s === 'error')
|
|
64
|
-
return { tone: 'red', word: t('editorStatus.word.error'), detail: t('editorStatus.live.error'), when: updatedAtLabel.value, error: props.live?.error ?? '' }
|
|
65
|
-
// An environment that never generates (dev / static output off) cannot resolve any pending state, so an
|
|
66
|
-
// alarming "Outdated" would point at a problem no action here can fix — calm neutral, even when an old
|
|
67
|
-
// success row (e.g. a prod run against the same DB) exists.
|
|
68
|
-
if (props.live?.generates === false)
|
|
69
|
-
return { tone: 'neutral', word: t('editorStatus.word.notBuilt'), detail: t('editorStatus.live.notBuilt') }
|
|
70
|
-
if (s === 'success') {
|
|
71
|
-
// Saved after the last publish: the file IS live, but it is an older version of this record. Saving no
|
|
72
|
-
// longer republishes (ADR-0008), so this is the normal state of a page being worked on — amber, not red.
|
|
73
|
-
if (props.live?.pending) {
|
|
74
|
-
return { tone: 'amber', word: t('editorStatus.word.outdated'), detail: t('editorStatus.live.outdated'), when: updatedAtLabel.value }
|
|
75
|
-
}
|
|
76
|
-
// Where the file actually landed — surface local vs S3 in the tooltip ("stored on S3").
|
|
77
|
-
const onS3 = (props.live?.target ?? props.live?.driver) === 's3'
|
|
78
|
-
return { tone: 'green', word: t('editorStatus.word.live'), detail: t(onS3 ? 'editorStatus.live.liveS3' : 'editorStatus.live.liveLocal'), when: updatedAtLabel.value }
|
|
79
|
-
}
|
|
80
|
-
// Never published, and a save does not enqueue anything (ADR-0008) — so nothing is in flight and
|
|
81
|
-
// nothing will be until someone presses Publish. Reporting progress here would point the user at a
|
|
82
|
-
// spinner instead of at the one action that changes it. With `publishOnSave` a save DOES republish, so
|
|
83
|
-
// there the in-flight reading is the right one.
|
|
84
|
-
if (props.live?.neverPublished && !props.live?.publishOnSave)
|
|
85
|
-
return { tone: 'blue', word: t('editorStatus.word.notPublished'), detail: t('editorStatus.live.notPublished') }
|
|
86
|
-
return { tone: 'amber', word: t('editorStatus.word.generating'), detail: t('editorStatus.live.pending') }
|
|
87
|
-
})
|
|
88
|
-
</script>
|
|
89
|
-
|
|
90
|
-
<template>
|
|
91
|
-
<span class="editor-status-group">
|
|
92
|
-
<!-- Polite live region: announces save-state transitions (Saved → Unsaved → Saving…) to screen readers.
|
|
93
|
-
Visually hidden; not a tooltip. -->
|
|
94
|
-
<span class="editor-status-sr" role="status" aria-live="polite">{{ save.detail }}</span>
|
|
95
|
-
|
|
96
|
-
<KestrelUiTooltip side="bottom">
|
|
97
|
-
<!-- A focusable status indicator, NOT a <button>: it has no action, and a button whose accessible
|
|
98
|
-
name contains "Saved"/"Unsaved" would collide with `getByRole('button', {name:'Save'})`. aria-label
|
|
99
|
-
leads with the visible word so it stays part of the accessible name, then adds the detail for AT. -->
|
|
100
|
-
<span class="editor-status" tabindex="0" :data-tone="save.tone" :aria-label="`${save.word} — ${save.detail}`">
|
|
101
|
-
<span class="editor-status__dot" aria-hidden="true" />
|
|
102
|
-
<span class="editor-status__word">{{ save.word }}</span>
|
|
103
|
-
</span>
|
|
104
|
-
<template #content>
|
|
105
|
-
<span class="editor-status-tip">
|
|
106
|
-
<strong class="editor-status-tip__detail">{{ save.detail }}</strong>
|
|
107
|
-
</span>
|
|
108
|
-
</template>
|
|
109
|
-
</KestrelUiTooltip>
|
|
110
|
-
|
|
111
|
-
<KestrelUiTooltip v-if="live" side="bottom">
|
|
112
|
-
<span class="editor-status-live" tabindex="0" :data-tone="live.tone" :aria-label="`${live.word} — ${live.detail}`">
|
|
113
|
-
<span class="editor-status-live__dot" aria-hidden="true" />
|
|
114
|
-
<span class="editor-status-live__word">{{ live.word }}</span>
|
|
115
|
-
</span>
|
|
116
|
-
<template #content>
|
|
117
|
-
<span class="editor-status-tip">
|
|
118
|
-
<strong class="editor-status-tip__detail">{{ live.detail }}</strong>
|
|
119
|
-
<span v-if="live.when" class="editor-status-tip__when">{{ live.when }}</span>
|
|
120
|
-
<span v-if="live.tone === 'red' && live.error" class="editor-status-tip__err">{{ live.error }}</span>
|
|
121
|
-
</span>
|
|
122
|
-
</template>
|
|
123
|
-
</KestrelUiTooltip>
|
|
124
|
-
</span>
|
|
125
|
-
</template>
|
|
126
|
-
|
|
127
|
-
<style scoped lang="scss">
|
|
128
|
-
.editor-status-group {
|
|
129
|
-
display: inline-flex;
|
|
130
|
-
align-items: center;
|
|
131
|
-
gap: var(--space-3);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Visually-hidden polite status region (mirrors the ui sr-only mixin; admin can't @use a ui-layer scss).
|
|
135
|
-
.editor-status-sr {
|
|
136
|
-
position: absolute;
|
|
137
|
-
width: 1px;
|
|
138
|
-
height: 1px;
|
|
139
|
-
padding: 0;
|
|
140
|
-
margin: -1px;
|
|
141
|
-
overflow: hidden;
|
|
142
|
-
clip: rect(0, 0, 0, 0);
|
|
143
|
-
white-space: nowrap;
|
|
144
|
-
border: 0;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Both lamps are focusable status indicators (tabindex spans, not buttons) so the tooltip opens on hover
|
|
148
|
-
// AND keyboard focus, without adding a "Save"-named button that would clash with Save-button selectors.
|
|
149
|
-
.editor-status,
|
|
150
|
-
.editor-status-live {
|
|
151
|
-
display: inline-flex;
|
|
152
|
-
align-items: center;
|
|
153
|
-
gap: var(--space-2);
|
|
154
|
-
cursor: help;
|
|
155
|
-
|
|
156
|
-
&__dot {
|
|
157
|
-
width: 0.6rem;
|
|
158
|
-
height: 0.6rem;
|
|
159
|
-
border-radius: var(--radius-full);
|
|
160
|
-
background: var(--_tone, var(--color-text-subtle));
|
|
161
|
-
box-shadow: 0 0 0 3px color-mix(in srgb, var(--_tone, transparent) 22%, transparent);
|
|
162
|
-
flex: none;
|
|
163
|
-
}
|
|
164
|
-
&__word {
|
|
165
|
-
font-size: var(--text-sm);
|
|
166
|
-
color: var(--color-text-muted);
|
|
167
|
-
white-space: nowrap;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
&[data-tone='amber'] {
|
|
171
|
-
--_tone: var(--color-warning);
|
|
172
|
-
}
|
|
173
|
-
&[data-tone='green'] {
|
|
174
|
-
--_tone: var(--color-success);
|
|
175
|
-
}
|
|
176
|
-
&[data-tone='blue'] {
|
|
177
|
-
--_tone: var(--color-primary);
|
|
178
|
-
}
|
|
179
|
-
&[data-tone='red'] {
|
|
180
|
-
--_tone: var(--color-danger);
|
|
181
|
-
}
|
|
182
|
-
&[data-tone='neutral'] {
|
|
183
|
-
--_tone: var(--color-text-subtle);
|
|
184
|
-
}
|
|
185
|
-
&:focus-visible {
|
|
186
|
-
outline: 2px solid var(--color-primary);
|
|
187
|
-
outline-offset: 2px;
|
|
188
|
-
border-radius: var(--radius-sm);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
</style>
|
|
192
|
-
|
|
193
|
-
<!-- Not scoped: the tooltip content is teleported to <body> by Reka, where a scoped data-v selector no
|
|
194
|
-
longer matches (same reason UiTooltip itself keeps global styles). -->
|
|
195
|
-
<style lang="scss">
|
|
196
|
-
.editor-status-tip {
|
|
197
|
-
display: flex;
|
|
198
|
-
flex-direction: column;
|
|
199
|
-
gap: var(--space-1);
|
|
200
|
-
|
|
201
|
-
&__when {
|
|
202
|
-
color: var(--color-text-muted);
|
|
203
|
-
font-size: var(--text-xs);
|
|
204
|
-
}
|
|
205
|
-
&__err {
|
|
206
|
-
color: var(--color-danger);
|
|
207
|
-
font-size: var(--text-xs);
|
|
208
|
-
word-break: break-word;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
</style>
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
// Fallback body when a collection's `editor` type has no registered body (an extension misconfig or a
|
|
3
|
-
// missing plugin). Fails visibly instead of rendering blank.
|
|
4
|
-
defineProps<{ editor: string }>()
|
|
5
|
-
const { t } = useT()
|
|
6
|
-
</script>
|
|
7
|
-
|
|
8
|
-
<template>
|
|
9
|
-
<div class="editor-unsupported">
|
|
10
|
-
<p class="editor-unsupported__note">{{ t('editor.unsupported', { type: editor }) }}</p>
|
|
11
|
-
</div>
|
|
12
|
-
</template>
|
|
13
|
-
|
|
14
|
-
<style lang="scss">
|
|
15
|
-
.editor-unsupported {
|
|
16
|
-
flex: 1 1 auto;
|
|
17
|
-
min-height: 0;
|
|
18
|
-
|
|
19
|
-
&__note {
|
|
20
|
-
font-size: var(--text-sm);
|
|
21
|
-
color: var(--color-text-muted);
|
|
22
|
-
padding: var(--space-3) var(--space-4);
|
|
23
|
-
border: 1px solid var(--color-border);
|
|
24
|
-
border-radius: var(--radius-md);
|
|
25
|
-
background: var(--color-surface-2);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
</style>
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
// The schema-driven default editor body: a plain field list. All state comes from the editor form
|
|
3
|
-
// context provided by CollectionEditor; this body only lays it out.
|
|
4
|
-
const ctx = useEditorFormContext()
|
|
5
|
-
</script>
|
|
6
|
-
|
|
7
|
-
<template>
|
|
8
|
-
<div class="editor__flat">
|
|
9
|
-
<KestrelPageFieldsPane v-bind="ctx.pageFieldsBindings.value" v-on="ctx.pageFieldsHandlers" />
|
|
10
|
-
</div>
|
|
11
|
-
</template>
|
|
12
|
-
|
|
13
|
-
<style lang="scss">
|
|
14
|
-
// Flat-collection field list: its own scroll region, so the shell's actions row stays pinned below it.
|
|
15
|
-
.editor__flat {
|
|
16
|
-
flex: 1 1 auto;
|
|
17
|
-
min-height: 0;
|
|
18
|
-
overflow-y: auto;
|
|
19
|
-
display: flex;
|
|
20
|
-
flex-direction: column;
|
|
21
|
-
gap: var(--space-4);
|
|
22
|
-
}
|
|
23
|
-
</style>
|