@michaelthielemann/kestrel 1.2.1
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/LICENSE +201 -0
- package/NOTICE +12 -0
- package/README.md +234 -0
- package/kestrel.config.ts +28 -0
- package/layers/access/nuxt.config.ts +5 -0
- package/layers/access/server/middleware/00.ip-allowlist.ts +67 -0
- package/layers/access/server/middleware/access-guard.ts +30 -0
- package/layers/access/server/types.d.ts +8 -0
- package/layers/access/server/utils/csrf.ts +39 -0
- package/layers/access/server/utils/grant-registry.ts +36 -0
- package/layers/access/server/utils/guard.ts +44 -0
- package/layers/access/server/utils/ip-allowlist.ts +76 -0
- package/layers/access/server/utils/policy.ts +98 -0
- package/layers/access/server/utils/public-resources.ts +11 -0
- package/layers/access/server/utils/render-context.ts +41 -0
- package/layers/access/server/utils/require-admin.ts +15 -0
- package/layers/admin/app/components/AdminAccount.vue +212 -0
- package/layers/admin/app/components/AdminNav.vue +44 -0
- package/layers/admin/app/components/BlockFields.vue +68 -0
- package/layers/admin/app/components/BlockPreview.vue +401 -0
- package/layers/admin/app/components/BlockTree.vue +433 -0
- package/layers/admin/app/components/BlocksBody.vue +162 -0
- package/layers/admin/app/components/CollectionDeleteDialog.vue +38 -0
- package/layers/admin/app/components/CollectionEditor.vue +203 -0
- package/layers/admin/app/components/CollectionList.vue +1091 -0
- package/layers/admin/app/components/EditorStatus.vue +199 -0
- package/layers/admin/app/components/EditorUnsupported.vue +28 -0
- package/layers/admin/app/components/FieldsBody.vue +23 -0
- package/layers/admin/app/components/LocaleBar.vue +105 -0
- package/layers/admin/app/components/PageFields.vue +101 -0
- package/layers/admin/app/components/PageFieldsPane.vue +39 -0
- package/layers/admin/app/components/SeoFields.vue +169 -0
- package/layers/admin/app/components/SingletonEditor.vue +92 -0
- package/layers/admin/app/composables/useAuth.ts +68 -0
- package/layers/admin/app/composables/useBlockTree.ts +95 -0
- package/layers/admin/app/composables/useBlocks.ts +7 -0
- package/layers/admin/app/composables/useCachedResource.ts +35 -0
- package/layers/admin/app/composables/useCollectionOps.ts +53 -0
- package/layers/admin/app/composables/useCollections.ts +7 -0
- package/layers/admin/app/composables/useContentLocales.ts +23 -0
- package/layers/admin/app/composables/useEditForm.ts +370 -0
- package/layers/admin/app/composables/useLinkResolver.ts +40 -0
- package/layers/admin/app/composables/useListColumns.ts +53 -0
- package/layers/admin/app/composables/useListUrlState.ts +53 -0
- package/layers/admin/app/composables/usePendingPublishPoll.ts +15 -0
- package/layers/admin/app/composables/usePreviewViewport.ts +30 -0
- package/layers/admin/app/composables/usePublishStatus.ts +98 -0
- package/layers/admin/app/composables/useRailCollapsed.ts +14 -0
- package/layers/admin/app/composables/useTheme.ts +36 -0
- package/layers/admin/app/composables/useUnsavedGuard.ts +22 -0
- package/layers/admin/app/layouts/admin.vue +267 -0
- package/layers/admin/app/middleware/admin-auth.ts +4 -0
- package/layers/admin/app/pages/admin/[collection]/[id].vue +202 -0
- package/layers/admin/app/pages/admin/[collection]/index.vue +62 -0
- package/layers/admin/app/pages/admin/index.vue +118 -0
- package/layers/admin/app/pages/admin/login.vue +68 -0
- package/layers/admin/app/pages/admin/media.vue +28 -0
- package/layers/admin/app/pages/admin/references.vue +146 -0
- package/layers/admin/app/plugins/admin-style-guard.client.ts +76 -0
- package/layers/admin/app/plugins/reauth.client.ts +20 -0
- package/layers/admin/app/utils/admin-style-guard.ts +20 -0
- package/layers/admin/app/utils/block-tree.ts +214 -0
- package/layers/admin/app/utils/collection-ops.ts +44 -0
- package/layers/admin/app/utils/dead-refs.ts +30 -0
- package/layers/admin/app/utils/edit-form.ts +223 -0
- package/layers/admin/app/utils/editor-expose.ts +26 -0
- package/layers/admin/app/utils/editor-form-context.ts +83 -0
- package/layers/admin/app/utils/editor-registry.ts +20 -0
- package/layers/admin/app/utils/field-keys.ts +11 -0
- package/layers/admin/app/utils/list-columns.ts +141 -0
- package/layers/admin/app/utils/list-query.ts +96 -0
- package/layers/admin/app/utils/nav-active.ts +9 -0
- package/layers/admin/app/utils/populate-blocks.ts +288 -0
- package/layers/admin/app/utils/preview-channel.ts +62 -0
- package/layers/admin/app/utils/preview-viewport.ts +80 -0
- package/layers/admin/app/utils/reauth.ts +68 -0
- package/layers/admin/app/utils/record-title.ts +15 -0
- package/layers/admin/app/utils/register-builtin-editors.ts +9 -0
- package/layers/admin/app/utils/safe-redirect.ts +12 -0
- package/layers/admin/nuxt.config.ts +17 -0
- package/layers/auth/nuxt.config.ts +1 -0
- package/layers/auth/server/api/auth/login.post.ts +38 -0
- package/layers/auth/server/api/auth/logout.post.ts +9 -0
- package/layers/auth/server/api/auth/session.get.ts +1 -0
- package/layers/auth/server/utils/admin-credential.ts +42 -0
- package/layers/auth/server/utils/client-ip.ts +88 -0
- package/layers/auth/server/utils/login-throttle.ts +126 -0
- package/layers/auth/server/utils/password.ts +52 -0
- package/layers/auth/server/utils/session-cookie.ts +44 -0
- package/layers/auth/server/utils/session-epoch.ts +96 -0
- package/layers/auth/server/utils/session.ts +85 -0
- package/layers/collections/nuxt.config.ts +1 -0
- package/layers/collections/server/collections/pages.ts +19 -0
- package/layers/collections/server/plugins/01.register.ts +32 -0
- package/layers/collections/server/plugins/02.register-relation-populate.ts +17 -0
- package/layers/collections/server/utils/populate-relations.ts +66 -0
- package/layers/core/app/utils/bulk-actions.ts +9 -0
- package/layers/core/app/utils/filter-ops.ts +98 -0
- package/layers/core/app/utils/list-limits.ts +27 -0
- package/layers/core/app/utils/locale-path.ts +12 -0
- package/layers/core/app/utils/slugify.ts +16 -0
- package/layers/core/modules/auto-discovery/extract-block.ts +176 -0
- package/layers/core/modules/auto-discovery/index.ts +44 -0
- package/layers/core/modules/auto-discovery/scan.ts +78 -0
- package/layers/core/modules/auto-discovery/virtual.d.ts +12 -0
- package/layers/core/modules/kestrel/index.ts +86 -0
- package/layers/core/nuxt.config.ts +8 -0
- package/layers/core/server/api/[collection]/[id]/dead-refs.get.ts +8 -0
- package/layers/core/server/api/[collection]/[id]/translations.get.ts +4 -0
- package/layers/core/server/api/[collection]/[id].delete.ts +5 -0
- package/layers/core/server/api/[collection]/[id].get.ts +8 -0
- package/layers/core/server/api/[collection]/[id].patch.ts +8 -0
- package/layers/core/server/api/[collection]/bulk.post.ts +37 -0
- package/layers/core/server/api/[collection]/index.get.ts +19 -0
- package/layers/core/server/api/[collection]/index.post.ts +8 -0
- package/layers/core/server/api/[collection]/index.put.ts +7 -0
- package/layers/core/server/api/[collection]/options.get.ts +18 -0
- package/layers/core/server/api/[collection]/translations.get.ts +25 -0
- package/layers/core/server/api/collections/[name].get.ts +4 -0
- package/layers/core/server/api/collections/index.get.ts +3 -0
- package/layers/core/server/api/references/broken.get.ts +3 -0
- package/layers/core/server/api/references/referrers.get.ts +24 -0
- package/layers/core/server/database/record-refs.ts +20 -0
- package/layers/core/server/plugins/00.migrate.ts +11 -0
- package/layers/core/server/plugins/02.schema-sync.ts +63 -0
- package/layers/core/server/plugins/03.record-refs.ts +12 -0
- package/layers/core/server/schema/bootstrap.ts +41 -0
- package/layers/core/server/schema/desired.ts +99 -0
- package/layers/core/server/schema/dev-sync.ts +23 -0
- package/layers/core/server/schema/dialect.ts +59 -0
- package/layers/core/server/schema/diff.ts +110 -0
- package/layers/core/server/schema/introspect.ts +67 -0
- package/layers/core/server/schema/model.ts +61 -0
- package/layers/core/server/schema/render-sqlite.ts +89 -0
- package/layers/core/server/schema/sync.ts +201 -0
- package/layers/core/server/tasks/db/migrate.ts +54 -0
- package/layers/core/server/utils/blocks.ts +6 -0
- package/layers/core/server/utils/collection-types.ts +27 -0
- package/layers/core/server/utils/crud.ts +577 -0
- package/layers/core/server/utils/db.ts +26 -0
- package/layers/core/server/utils/defineCollection.ts +207 -0
- package/layers/core/server/utils/duplicate.ts +162 -0
- package/layers/core/server/utils/field-layout.ts +125 -0
- package/layers/core/server/utils/filter-predicate.ts +101 -0
- package/layers/core/server/utils/http.ts +50 -0
- package/layers/core/server/utils/if-unmodified.ts +13 -0
- package/layers/core/server/utils/kestrel-config.ts +410 -0
- package/layers/core/server/utils/key-lock.ts +33 -0
- package/layers/core/server/utils/locale.ts +53 -0
- package/layers/core/server/utils/page-route.ts +58 -0
- package/layers/core/server/utils/page-slug.ts +127 -0
- package/layers/core/server/utils/picker.ts +82 -0
- package/layers/core/server/utils/populate.ts +48 -0
- package/layers/core/server/utils/prerender-memo.ts +23 -0
- package/layers/core/server/utils/read-capture.ts +37 -0
- package/layers/core/server/utils/record-ref-index.ts +200 -0
- package/layers/core/server/utils/registry.ts +24 -0
- package/layers/core/server/utils/resolve-scope.ts +79 -0
- package/layers/core/server/utils/seo.ts +24 -0
- package/layers/core/server/utils/serialize-collection.ts +149 -0
- package/layers/core/server/utils/server-config.ts +18 -0
- package/layers/core/server/utils/sql.ts +2 -0
- package/layers/core/server/utils/storage.local.ts +117 -0
- package/layers/core/server/utils/storage.s3.ts +197 -0
- package/layers/core/server/utils/storage.ts +90 -0
- package/layers/core/server/utils/write-events.ts +31 -0
- package/layers/fields/app/utils/condition.ts +71 -0
- package/layers/fields/app/utils/define-block.ts +13 -0
- package/layers/fields/app/utils/field-constraints.ts +17 -0
- package/layers/fields/app/utils/field-factories.ts +129 -0
- package/layers/fields/app/utils/field-meta.ts +33 -0
- package/layers/fields/app/utils/field-validate.ts +75 -0
- package/layers/fields/app/utils/field-value.ts +7 -0
- package/layers/fields/app/utils/richtext-links.ts +48 -0
- package/layers/fields/nuxt.config.ts +1 -0
- package/layers/fields/server/api/blocks/index.get.ts +9 -0
- package/layers/fields/server/field-registry/index.ts +248 -0
- package/layers/fields/server/field-registry/naming.ts +10 -0
- package/layers/fields/server/field-registry/sanitize.ts +40 -0
- package/layers/fields/server/field-registry/types.ts +19 -0
- package/layers/fields/server/plugins/01.register-field-populate.ts +11 -0
- package/layers/fields/server/utils/block-ids.ts +43 -0
- package/layers/fields/server/utils/block-populate.ts +41 -0
- package/layers/fields/server/utils/buildCollection.ts +88 -0
- package/layers/fields/server/utils/buildFieldSchema.ts +22 -0
- package/layers/fields/server/utils/buildTable.ts +107 -0
- package/layers/fields/server/utils/conditional-required.ts +25 -0
- package/layers/fields/server/utils/defineBlock.ts +86 -0
- package/layers/fields/server/utils/defineFieldType.ts +33 -0
- package/layers/fields/server/utils/extract-refs.ts +152 -0
- package/layers/fields/server/utils/field-populate.ts +68 -0
- package/layers/media/app/components/KestrelImg.vue +65 -0
- package/layers/media/app/components/MediaConflictDialog.vue +35 -0
- package/layers/media/app/components/MediaDeleteDialog.vue +52 -0
- package/layers/media/app/components/MediaGrid.vue +113 -0
- package/layers/media/app/components/MediaImage.vue +70 -0
- package/layers/media/app/components/MediaLibrary.vue +451 -0
- package/layers/media/app/components/MediaNewFolderDialog.vue +32 -0
- package/layers/media/app/components/MediaPathBar.vue +163 -0
- package/layers/media/app/components/MediaPicker.vue +19 -0
- package/layers/media/app/components/MediaRenameDialog.vue +32 -0
- package/layers/media/app/components/MediaTable.vue +75 -0
- package/layers/media/app/components/MediaThumb.vue +25 -0
- package/layers/media/app/components/MediaToolbar.vue +90 -0
- package/layers/media/app/components/MediaUploadDialog.vue +87 -0
- package/layers/media/app/components/MediaViewer.vue +88 -0
- package/layers/media/app/components/field/Media.vue +261 -0
- package/layers/media/app/composables/useBlurUp.ts +55 -0
- package/layers/media/app/composables/useMediaClipboard.ts +14 -0
- package/layers/media/app/composables/useMediaContextMenu.ts +56 -0
- package/layers/media/app/composables/useMediaDnd.ts +56 -0
- package/layers/media/app/composables/useMediaLibrary.ts +151 -0
- package/layers/media/app/composables/useMediaOps.ts +50 -0
- package/layers/media/app/composables/useMediaRelocate.ts +39 -0
- package/layers/media/app/composables/useMediaResolver.ts +50 -0
- package/layers/media/app/composables/useMediaUpload.ts +93 -0
- package/layers/media/app/composables/useMediaVariant.ts +34 -0
- package/layers/media/app/plugins/field-media.client.ts +6 -0
- package/layers/media/app/utils/dnd.ts +69 -0
- package/layers/media/app/utils/library.ts +79 -0
- package/layers/media/app/utils/ops.ts +62 -0
- package/layers/media/app/utils/picture-model.ts +125 -0
- package/layers/media/app/utils/thumbhash.ts +19 -0
- package/layers/media/app/utils/variant-name.ts +20 -0
- package/layers/media/nuxt.config.ts +3 -0
- package/layers/media/server/api/media/[id]/usages.get.ts +8 -0
- package/layers/media/server/api/media/[id].delete.ts +18 -0
- package/layers/media/server/api/media/[id].get.ts +10 -0
- package/layers/media/server/api/media/[id].patch.ts +41 -0
- package/layers/media/server/api/media/backfill.post.ts +18 -0
- package/layers/media/server/api/media/copy.post.ts +7 -0
- package/layers/media/server/api/media/delete.post.ts +13 -0
- package/layers/media/server/api/media/folders.post.ts +18 -0
- package/layers/media/server/api/media/index.get.ts +15 -0
- package/layers/media/server/api/media/index.post.ts +157 -0
- package/layers/media/server/api/media/library.get.ts +17 -0
- package/layers/media/server/api/media/move.post.ts +7 -0
- package/layers/media/server/api/media/rename.post.ts +7 -0
- package/layers/media/server/api/media/resolve.get.ts +13 -0
- package/layers/media/server/collections/media-settings.ts +25 -0
- package/layers/media/server/collections/media.ts +29 -0
- package/layers/media/server/database/folders.ts +10 -0
- package/layers/media/server/middleware/ondemand-variants.ts +37 -0
- package/layers/media/server/plugins/02.register-media.ts +19 -0
- package/layers/media/server/plugins/03.media-cleanup.ts +20 -0
- package/layers/media/server/plugins/04.variant-capture.ts +23 -0
- package/layers/media/server/schema-tables/folders.ts +4 -0
- package/layers/media/server/tasks/media/backfill.ts +18 -0
- package/layers/media/server/utils/backfill.ts +132 -0
- package/layers/media/server/utils/derive.ts +59 -0
- package/layers/media/server/utils/folder-paths.ts +42 -0
- package/layers/media/server/utils/folders.ts +12 -0
- package/layers/media/server/utils/library.ts +90 -0
- package/layers/media/server/utils/media-cleanup.ts +27 -0
- package/layers/media/server/utils/media-enabled.ts +23 -0
- package/layers/media/server/utils/media-ops.ts +118 -0
- package/layers/media/server/utils/media-write.ts +13 -0
- package/layers/media/server/utils/naming.ts +52 -0
- package/layers/media/server/utils/ondemand.ts +69 -0
- package/layers/media/server/utils/persist-upload.ts +62 -0
- package/layers/media/server/utils/populate.ts +47 -0
- package/layers/media/server/utils/record.ts +63 -0
- package/layers/media/server/utils/relocate-ops.ts +341 -0
- package/layers/media/server/utils/relocate-plan.ts +63 -0
- package/layers/media/server/utils/resolve.ts +86 -0
- package/layers/media/server/utils/sanitize-svg.ts +12 -0
- package/layers/media/server/utils/sniff.ts +68 -0
- package/layers/media/server/utils/storage-relocate.ts +139 -0
- package/layers/media/server/utils/translations.ts +19 -0
- package/layers/media/server/utils/usages.ts +66 -0
- package/layers/media/server/utils/variant-capture.ts +70 -0
- package/layers/media/server/utils/variants.ts +108 -0
- package/layers/public/app/app.vue +5 -0
- package/layers/public/app/components/BlockRenderer.vue +56 -0
- package/layers/public/app/components/KestrelLink.vue +26 -0
- package/layers/public/app/components/KestrelPreviewBridge.vue +133 -0
- package/layers/public/app/composables/public-page.ts +24 -0
- package/layers/public/app/error.vue +12 -0
- package/layers/public/app/layouts/default.vue +5 -0
- package/layers/public/app/pages/[...slug].vue +157 -0
- package/layers/public/app/pages/__kestrel/preview.vue +34 -0
- package/layers/public/app/utils/block-edit-context.ts +16 -0
- package/layers/public/app/utils/link-href.ts +46 -0
- package/layers/public/app/utils/page-head.ts +84 -0
- package/layers/public/app/utils/preview-protocol.ts +81 -0
- package/layers/public/app/utils/public-route.ts +46 -0
- package/layers/public/modules/deploy-output/deploy-output.ts +370 -0
- package/layers/public/modules/deploy-output/index.ts +82 -0
- package/layers/public/modules/prerender-routes/discover.ts +37 -0
- package/layers/public/modules/prerender-routes/index.ts +85 -0
- package/layers/public/modules/prerender-routes/routes.ts +16 -0
- package/layers/public/modules/prune-media/index.ts +48 -0
- package/layers/public/modules/prune-media/prune-media.ts +81 -0
- package/layers/public/nuxt.config.ts +11 -0
- package/layers/public/server/api/links/resolve.get.ts +19 -0
- package/layers/public/server/api/publish-status.get.ts +44 -0
- package/layers/public/server/api/route.get.ts +18 -0
- package/layers/public/server/database/publish-deps.ts +12 -0
- package/layers/public/server/database/publish-status.ts +19 -0
- package/layers/public/server/plugins/02.register-links.ts +17 -0
- package/layers/public/server/plugins/zz.publish.ts +63 -0
- package/layers/public/server/routes/llms.txt.get.ts +72 -0
- package/layers/public/server/routes/robots.txt.get.ts +8 -0
- package/layers/public/server/routes/sitemap.xml.get.ts +66 -0
- package/layers/public/server/schema-tables/publish-deps.ts +4 -0
- package/layers/public/server/schema-tables/publish-status.ts +4 -0
- package/layers/public/server/tasks/publish/run.ts +26 -0
- package/layers/public/server/utils/link-resolve.ts +35 -0
- package/layers/public/server/utils/llms.ts +39 -0
- package/layers/public/server/utils/page-resolve.ts +86 -0
- package/layers/public/server/utils/populate-links.ts +66 -0
- package/layers/public/server/utils/publish/coalesce.ts +22 -0
- package/layers/public/server/utils/publish/deps-persistence.ts +43 -0
- package/layers/public/server/utils/publish/deps.ts +87 -0
- package/layers/public/server/utils/publish/invalidation.ts +141 -0
- package/layers/public/server/utils/publish/publish-status.ts +54 -0
- package/layers/public/server/utils/publish/publisher.ts +291 -0
- package/layers/public/server/utils/publish/queue.ts +67 -0
- package/layers/public/server/utils/publish/route-for-record.ts +16 -0
- package/layers/public/server/utils/publish/route-keys.ts +13 -0
- package/layers/public/server/utils/site-url.ts +32 -0
- package/layers/public/server/utils/sitemap.ts +97 -0
- package/layers/ui/app/assets/scss/_base.scss +16 -0
- package/layers/ui/app/assets/scss/_datepicker.scss +82 -0
- package/layers/ui/app/assets/scss/_mixins.scss +50 -0
- package/layers/ui/app/assets/scss/_reset.scss +65 -0
- package/layers/ui/app/assets/scss/_tokens.scss +183 -0
- package/layers/ui/app/assets/scss/main.scss +4 -0
- package/layers/ui/app/components/field/Boolean.vue +30 -0
- package/layers/ui/app/components/field/Choice.vue +68 -0
- package/layers/ui/app/components/field/Datetime.vue +96 -0
- package/layers/ui/app/components/field/DeadRefNote.vue +28 -0
- package/layers/ui/app/components/field/Json.vue +63 -0
- package/layers/ui/app/components/field/Layout.vue +140 -0
- package/layers/ui/app/components/field/Link.vue +112 -0
- package/layers/ui/app/components/field/LinkInternalPicker.vue +95 -0
- package/layers/ui/app/components/field/Number.vue +30 -0
- package/layers/ui/app/components/field/Relation.vue +49 -0
- package/layers/ui/app/components/field/Renderer.vue +30 -0
- package/layers/ui/app/components/field/Repeater.vue +460 -0
- package/layers/ui/app/components/field/Richtext.vue +26 -0
- package/layers/ui/app/components/field/Slug.vue +66 -0
- package/layers/ui/app/components/field/Text.vue +35 -0
- package/layers/ui/app/components/field/Unsupported.vue +34 -0
- package/layers/ui/app/components/ui/Alert.vue +50 -0
- package/layers/ui/app/components/ui/Brand.vue +22 -0
- package/layers/ui/app/components/ui/Button.vue +116 -0
- package/layers/ui/app/components/ui/ButtonGroup.vue +96 -0
- package/layers/ui/app/components/ui/Checkbox.vue +52 -0
- package/layers/ui/app/components/ui/CheckboxGroup.vue +50 -0
- package/layers/ui/app/components/ui/Combobox.vue +343 -0
- package/layers/ui/app/components/ui/DatePicker.vue +92 -0
- package/layers/ui/app/components/ui/DateRangePicker.vue +100 -0
- package/layers/ui/app/components/ui/Dialog.vue +107 -0
- package/layers/ui/app/components/ui/EmptyState.vue +46 -0
- package/layers/ui/app/components/ui/Field.vue +57 -0
- package/layers/ui/app/components/ui/Fieldset.vue +64 -0
- package/layers/ui/app/components/ui/Icon.vue +44 -0
- package/layers/ui/app/components/ui/Menu.vue +54 -0
- package/layers/ui/app/components/ui/NumberInput.vue +173 -0
- package/layers/ui/app/components/ui/Richtext.vue +119 -0
- package/layers/ui/app/components/ui/RichtextToolbar.vue +253 -0
- package/layers/ui/app/components/ui/Select.vue +51 -0
- package/layers/ui/app/components/ui/TextInput.vue +110 -0
- package/layers/ui/app/components/ui/Textarea.vue +38 -0
- package/layers/ui/app/components/ui/TimeInput.vue +17 -0
- package/layers/ui/app/components/ui/Toasts.vue +103 -0
- package/layers/ui/app/components/ui/Tooltip.vue +45 -0
- package/layers/ui/app/composables/useAdminLang.ts +8 -0
- package/layers/ui/app/composables/useDragReorder.ts +41 -0
- package/layers/ui/app/composables/useEchoGuard.ts +28 -0
- package/layers/ui/app/composables/useLinkField.ts +70 -0
- package/layers/ui/app/composables/useRecordOptions.ts +84 -0
- package/layers/ui/app/composables/useRepeater.ts +99 -0
- package/layers/ui/app/composables/useT.ts +32 -0
- package/layers/ui/app/composables/useToast.ts +46 -0
- package/layers/ui/app/i18n/de.ts +406 -0
- package/layers/ui/app/i18n/en.ts +440 -0
- package/layers/ui/app/utils/boolean-field.ts +10 -0
- package/layers/ui/app/utils/field-component.ts +22 -0
- package/layers/ui/app/utils/field-empty.ts +21 -0
- package/layers/ui/app/utils/field-registry.ts +36 -0
- package/layers/ui/app/utils/humanize.ts +14 -0
- package/layers/ui/app/utils/icons.ts +147 -0
- package/layers/ui/app/utils/iso-date.ts +18 -0
- package/layers/ui/app/utils/localized.ts +13 -0
- package/layers/ui/app/utils/record-context.ts +15 -0
- package/layers/ui/app/utils/reorder.ts +8 -0
- package/layers/ui/app/utils/richtext-preserve-class.ts +31 -0
- package/layers/ui/app/utils/useFieldA11y.ts +40 -0
- package/layers/ui/nuxt.config.ts +5 -0
- package/nuxt.config.ts +105 -0
- package/package.json +122 -0
- package/scripts/hash-password.mjs +26 -0
- package/scripts/typecheck.mjs +51 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// A proper editor for a single-record (singleton) collection — the same header-driven chrome as the
|
|
3
|
+
// record editor, minus list/new/delete. The header carries the title and a Save button that submits the
|
|
4
|
+
// CollectionEditor's form by id; `saving`/`dirty` come from the editor's exposed state.
|
|
5
|
+
import type { EditorExpose } from '../utils/editor-expose'
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{ collection: string; title: string; localeParam?: string }>()
|
|
8
|
+
|
|
9
|
+
const { t } = useT()
|
|
10
|
+
const toast = useToast()
|
|
11
|
+
|
|
12
|
+
const EDITOR_FORM_ID = 'singleton-editor'
|
|
13
|
+
const editorRef = ref<EditorExpose | null>(null)
|
|
14
|
+
const saving = computed(() => editorRef.value?.saving ?? false)
|
|
15
|
+
|
|
16
|
+
function onSaved() {
|
|
17
|
+
toast.success(t('toast.saved'))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function openPreview() {
|
|
21
|
+
const url = editorRef.value?.previewUrl
|
|
22
|
+
if (url) window.open(url, '_blank', 'noopener,noreferrer')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Guard in-app navigation away from unsaved changes — both record-change AND same-record `?locale`
|
|
26
|
+
// switches (the LocaleBar's singleton locale switch reuses this route record → onBeforeRouteUpdate).
|
|
27
|
+
// The editor itself guards native unload.
|
|
28
|
+
useUnsavedGuard(() => editorRef.value?.dirty ?? false, () => t('editor.discardConfirm'))
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<section class="singleton">
|
|
33
|
+
<div class="singleton__head">
|
|
34
|
+
<h1 class="singleton__title">{{ title }}</h1>
|
|
35
|
+
<div class="singleton__actions">
|
|
36
|
+
<UiButton type="button" variant="ghost" size="sm" icon="undo" :disabled="saving || !editorRef?.canUndo" :title="t('history.undo')" :aria-label="t('history.undo')" @click="editorRef?.undo()" />
|
|
37
|
+
<UiButton type="button" variant="ghost" size="sm" icon="redo" :disabled="saving || !editorRef?.canRedo" :title="t('history.redo')" :aria-label="t('history.redo')" @click="editorRef?.redo()" />
|
|
38
|
+
<UiButton v-if="editorRef?.previewUrl" type="button" variant="ghost" size="sm" icon="external-link" :title="t('editor.openInNewTab')" :aria-label="t('editor.openInNewTab')" @click="openPreview" />
|
|
39
|
+
<UiButton type="submit" :form="EDITOR_FORM_ID" variant="primary" size="sm" icon="check" :loading="saving">{{ t('common.save') }}</UiButton>
|
|
40
|
+
<EditorStatus class="singleton__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" />
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
<CollectionEditor
|
|
44
|
+
ref="editorRef"
|
|
45
|
+
:collection="collection"
|
|
46
|
+
id="single"
|
|
47
|
+
:form-id="EDITOR_FORM_ID"
|
|
48
|
+
:locale-param="localeParam"
|
|
49
|
+
:actions="false"
|
|
50
|
+
@saved="onSaved"
|
|
51
|
+
/>
|
|
52
|
+
</section>
|
|
53
|
+
</template>
|
|
54
|
+
|
|
55
|
+
<style lang="scss">
|
|
56
|
+
// The singleton shell stays fixed and fills the height handed down; the editor body (.editor__flat /
|
|
57
|
+
// .editor3) owns the scroll — same model as the record editor (see the admin layout).
|
|
58
|
+
.singleton {
|
|
59
|
+
display: flex;
|
|
60
|
+
flex-direction: column;
|
|
61
|
+
gap: var(--space-4);
|
|
62
|
+
flex: 1 1 auto;
|
|
63
|
+
min-height: 0;
|
|
64
|
+
overflow: hidden;
|
|
65
|
+
|
|
66
|
+
// Fixed header; the fields scroll below it, so it needs no sticky positioning (matching .record__head).
|
|
67
|
+
&__head {
|
|
68
|
+
flex: 0 0 auto;
|
|
69
|
+
display: flex;
|
|
70
|
+
align-items: center;
|
|
71
|
+
justify-content: space-between;
|
|
72
|
+
gap: var(--space-4);
|
|
73
|
+
padding-bottom: var(--space-3);
|
|
74
|
+
border-bottom: 1px solid var(--color-border);
|
|
75
|
+
}
|
|
76
|
+
&__title {
|
|
77
|
+
font-size: var(--text-xl);
|
|
78
|
+
font-weight: var(--weight-bold);
|
|
79
|
+
text-transform: capitalize;
|
|
80
|
+
}
|
|
81
|
+
&__actions {
|
|
82
|
+
display: flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
gap: var(--space-2);
|
|
85
|
+
}
|
|
86
|
+
&__ampel {
|
|
87
|
+
margin-inline-start: var(--space-1);
|
|
88
|
+
padding-inline-start: var(--space-3);
|
|
89
|
+
border-inline-start: 1px solid var(--color-border);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
</style>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
interface AuthState {
|
|
2
|
+
authenticated: boolean
|
|
3
|
+
exp: number | null
|
|
4
|
+
checked: boolean
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function useAuth() {
|
|
8
|
+
const state = useState<AuthState>('kestrel-auth', () => ({
|
|
9
|
+
authenticated: false,
|
|
10
|
+
exp: null,
|
|
11
|
+
checked: false,
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
const expired = () => state.value.exp !== null && state.value.exp <= Date.now()
|
|
15
|
+
|
|
16
|
+
async function checkSession() {
|
|
17
|
+
// Fail closed: a transient session-endpoint failure (5xx, offline) must read as
|
|
18
|
+
// "not authenticated" so the route guard redirects to login instead of throwing an error page.
|
|
19
|
+
try {
|
|
20
|
+
const r = await $fetch<{ authenticated: boolean; exp?: number }>('/api/auth/session')
|
|
21
|
+
state.value = { authenticated: r.authenticated, exp: r.exp ?? null, checked: true }
|
|
22
|
+
} catch {
|
|
23
|
+
state.value = { authenticated: false, exp: null, checked: true }
|
|
24
|
+
}
|
|
25
|
+
return state.value.authenticated
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function ensureSession() {
|
|
29
|
+
if (!state.value.checked || expired()) await checkSession()
|
|
30
|
+
return state.value.authenticated && !expired()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function login(password: string) {
|
|
34
|
+
const r = await $fetch<{ ok: true; exp: number }>('/api/auth/login', {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
body: { password },
|
|
37
|
+
})
|
|
38
|
+
state.value = { authenticated: true, exp: r.exp, checked: true }
|
|
39
|
+
return r
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Drop the local session without a server round-trip — used by the re-auth interceptor on a 401. */
|
|
43
|
+
function reset() {
|
|
44
|
+
state.value = { authenticated: false, exp: null, checked: true }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function logout() {
|
|
48
|
+
// The desired end state is "logged out" regardless of the response: an expired/rotated
|
|
49
|
+
// session makes the guarded logout endpoint 401, but the local session is still cleared.
|
|
50
|
+
try {
|
|
51
|
+
await $fetch('/api/auth/logout', { method: 'POST' })
|
|
52
|
+
} catch {
|
|
53
|
+
// already invalid server-side — nothing to clear there
|
|
54
|
+
}
|
|
55
|
+
state.value = { authenticated: false, exp: null, checked: true }
|
|
56
|
+
await navigateTo('/admin/login')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
authenticated: computed(() => state.value.authenticated && !expired()),
|
|
61
|
+
exp: computed(() => state.value.exp),
|
|
62
|
+
checkSession,
|
|
63
|
+
ensureSession,
|
|
64
|
+
login,
|
|
65
|
+
logout,
|
|
66
|
+
reset,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { ref, computed } from 'vue'
|
|
2
|
+
import type { Ref } from 'vue'
|
|
3
|
+
import { useEchoGuard } from '../../../ui/app/composables/useEchoGuard'
|
|
4
|
+
import type { SerializedBlock } from '../../../core/server/utils/serialize-collection'
|
|
5
|
+
import {
|
|
6
|
+
type BlockRow,
|
|
7
|
+
findInTree,
|
|
8
|
+
updatePropById,
|
|
9
|
+
removeById,
|
|
10
|
+
removalRetarget,
|
|
11
|
+
moveById,
|
|
12
|
+
duplicateById,
|
|
13
|
+
addBlock,
|
|
14
|
+
} from '../utils/block-tree'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Reactive state for the 3-pane block editor: one nested `content` tree plus a single shared selection
|
|
18
|
+
* (the stable `block.id`, or `null` for the page root). The tree pane and the fields pane both drive
|
|
19
|
+
* the same instance — the tree selects + does structural ops, the fields pane edits the selected
|
|
20
|
+
* block's props. Every mutation is id-addressed and immutable (see `block-tree.ts`) and re-emits the
|
|
21
|
+
* whole array (so `useEditForm`'s `setField('content', …)` reconciliation still runs). `genId` is
|
|
22
|
+
* injectable for deterministic tests; defaults to `crypto.randomUUID` (the server requires a non-empty id).
|
|
23
|
+
*/
|
|
24
|
+
export function useBlockTree(
|
|
25
|
+
model: Ref<unknown[] | null | undefined>,
|
|
26
|
+
schemas: Ref<Record<string, SerializedBlock>>,
|
|
27
|
+
genId: () => string = () => crypto.randomUUID(),
|
|
28
|
+
// Routes an emit through the caller's own undo-history capture point instead of a plain `model.value =`
|
|
29
|
+
// assignment, tagged with a per-op coalesce key (see `emit`) — so a structural op landing inside another
|
|
30
|
+
// edit's coalesce window (e.g. a delete right after typing) still gets its own undo step.
|
|
31
|
+
setContent?: (value: unknown[], coalesceAs: string) => void,
|
|
32
|
+
) {
|
|
33
|
+
// JSON round-trip (not structuredClone — which throws on Vue reactive proxies): reads through the
|
|
34
|
+
// proxy into detached plain objects. Block content is JSON by definition, so this is lossless here.
|
|
35
|
+
const clone = (v: unknown): BlockRow[] => (Array.isArray(v) ? (JSON.parse(JSON.stringify(v)) as BlockRow[]) : [])
|
|
36
|
+
|
|
37
|
+
const blocks = ref<BlockRow[]>(clone(model.value))
|
|
38
|
+
const selectedId = ref<string | null>(null)
|
|
39
|
+
|
|
40
|
+
function emit(coalesceAs: string): void {
|
|
41
|
+
if (setContent) setContent(blocks.value, coalesceAs)
|
|
42
|
+
else model.value = blocks.value
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Echo-guard against the tree's OWN emit (shared primitive): an external change (load / locale copy /
|
|
46
|
+
// undo / redo) differs from the current tree, so it reseeds and drops a now-missing selection to root.
|
|
47
|
+
useEchoGuard(model, () => blocks.value, (v) => {
|
|
48
|
+
blocks.value = clone(v)
|
|
49
|
+
if (selectedId.value !== null && !findInTree(blocks.value, selectedId.value)) selectedId.value = null
|
|
50
|
+
}, [])
|
|
51
|
+
|
|
52
|
+
const selectedBlock = computed<BlockRow | null>(() =>
|
|
53
|
+
selectedId.value === null ? null : (findInTree(blocks.value, selectedId.value)?.block ?? null),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
function select(id: string | null): void {
|
|
57
|
+
selectedId.value = id
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function setProp(id: string, key: string, value: unknown): void {
|
|
61
|
+
blocks.value = updatePropById(blocks.value, id, key, value)
|
|
62
|
+
// Coalesces a typing burst into that one field, same as an ordinary text field would.
|
|
63
|
+
emit(`content:prop:${id}:${key}`)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function remove(id: string): void {
|
|
67
|
+
const retarget = removalRetarget(blocks.value, id)
|
|
68
|
+
blocks.value = removeById(blocks.value, id)
|
|
69
|
+
// Heal the selection whenever it no longer exists — covers removing the selected block itself AND
|
|
70
|
+
// removing an ANCESTOR of it (the whole subtree goes); land on the removed block's sibling/parent.
|
|
71
|
+
if (selectedId.value !== null && !findInTree(blocks.value, selectedId.value)) selectedId.value = retarget
|
|
72
|
+
emit(`content:remove:${id}`)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function move(id: string, dir: -1 | 1): void {
|
|
76
|
+
blocks.value = moveById(blocks.value, id, dir)
|
|
77
|
+
emit(`content:move:${id}`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function duplicate(id: string): void {
|
|
81
|
+
const { tree, newId } = duplicateById(blocks.value, id, genId)
|
|
82
|
+
blocks.value = tree
|
|
83
|
+
selectedId.value = newId
|
|
84
|
+
emit(`content:duplicate:${id}`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function add(parentId: string | null, slotName: string | null, type: string): void {
|
|
88
|
+
const { tree, newId } = addBlock(blocks.value, parentId, slotName, type, schemas.value, genId)
|
|
89
|
+
blocks.value = tree
|
|
90
|
+
selectedId.value = newId
|
|
91
|
+
emit(`content:add:${parentId}:${slotName}:${type}`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { blocks, selectedId, selectedBlock, select, setProp, remove, move, duplicate, add }
|
|
95
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SerializedBlock } from '../../../core/server/utils/serialize-collection'
|
|
2
|
+
|
|
3
|
+
// Thin wrapper over the shared cached-resource loader; keeps the named `blocks` return.
|
|
4
|
+
export function useBlocks() {
|
|
5
|
+
const { state: blocks, load } = useCachedResource<SerializedBlock>('kestrel-blocks', '/api/blocks')
|
|
6
|
+
return { blocks, load }
|
|
7
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Generic cached-resource loader: a `useState`-backed list that fetches once and is shared by every
|
|
2
|
+
// caller. Backs `useBlocks` / `useCollections` (structurally identical loaders).
|
|
3
|
+
//
|
|
4
|
+
// SSR isolation: the in-flight (dedup) promise is keyed first by the per-request Nuxt app (a WeakMap,
|
|
5
|
+
// so it can't leak across SSR requests) AND then by `stateKey` — otherwise two different resources
|
|
6
|
+
// loading concurrently in ONE request would share a single promise and cross-contaminate (one
|
|
7
|
+
// resolves with the other's data).
|
|
8
|
+
const inflight = new WeakMap<object, Map<string, Promise<unknown>>>()
|
|
9
|
+
|
|
10
|
+
export function useCachedResource<T>(stateKey: string, url: string) {
|
|
11
|
+
const state = useState<T[] | null>(stateKey, () => null)
|
|
12
|
+
const nuxtApp = useNuxtApp()
|
|
13
|
+
|
|
14
|
+
async function load(): Promise<T[]> {
|
|
15
|
+
if (state.value) return state.value
|
|
16
|
+
let perApp = inflight.get(nuxtApp)
|
|
17
|
+
if (!perApp) {
|
|
18
|
+
perApp = new Map()
|
|
19
|
+
inflight.set(nuxtApp, perApp)
|
|
20
|
+
}
|
|
21
|
+
let pending = perApp.get(stateKey) as Promise<T[]> | undefined
|
|
22
|
+
if (!pending) {
|
|
23
|
+
pending = $fetch<{ data: T[] }>(url)
|
|
24
|
+
.then(({ data }) => {
|
|
25
|
+
state.value = data
|
|
26
|
+
return data
|
|
27
|
+
})
|
|
28
|
+
.finally(() => perApp!.delete(stateKey))
|
|
29
|
+
perApp.set(stateKey, pending)
|
|
30
|
+
}
|
|
31
|
+
return pending
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { state, load }
|
|
35
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ref, toValue, type MaybeRefOrGetter } from 'vue'
|
|
2
|
+
import { buildDeleteReport, type BatchAction, type BatchDeleteReport, type BulkResult, type ReferrerCounts } from '../utils/collection-ops'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Client orchestration for the collection batch operations — ONE module, two entry points: a row action
|
|
6
|
+
* passes a single id, the bulk bar passes many. Every mutating call hits the one command endpoint
|
|
7
|
+
* (`POST /api/{collection}/bulk`); the server does the batching, the client never loops N requests.
|
|
8
|
+
* Mirrors media/useMediaOps (busy/error + an onChanged refetch hook).
|
|
9
|
+
*/
|
|
10
|
+
export function useCollectionOps(collection: MaybeRefOrGetter<string>, onChanged?: () => void | Promise<void>) {
|
|
11
|
+
const { t } = useT()
|
|
12
|
+
const busy = ref(false)
|
|
13
|
+
const error = ref<string | null>(null)
|
|
14
|
+
const name = () => toValue(collection)
|
|
15
|
+
|
|
16
|
+
async function bulk(action: BatchAction, ids: number[]): Promise<BulkResult> {
|
|
17
|
+
busy.value = true
|
|
18
|
+
error.value = null
|
|
19
|
+
try {
|
|
20
|
+
const res = await $fetch<BulkResult>(`/api/${name()}/bulk`, { method: 'POST', body: { action, ids } })
|
|
21
|
+
await onChanged?.()
|
|
22
|
+
return res
|
|
23
|
+
} catch (e) {
|
|
24
|
+
error.value = (e as { statusMessage?: string })?.statusMessage ?? t('list.opFailed')
|
|
25
|
+
throw e
|
|
26
|
+
} finally {
|
|
27
|
+
busy.value = false
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Read-only pre-delete summary (referrer aggregate). Best-effort: sets `error` and rethrows on failure
|
|
32
|
+
* so the caller can decide whether to still proceed (deletion is not blocked on the warning lookup). */
|
|
33
|
+
async function previewDelete(ids: number[]): Promise<BatchDeleteReport> {
|
|
34
|
+
error.value = null
|
|
35
|
+
try {
|
|
36
|
+
const res = await $fetch<ReferrerCounts>('/api/references/referrers', { query: { collection: name(), ids: ids.join(',') } })
|
|
37
|
+
// The endpoint flags whether the index lookup ran; carry that through rather than inferring safety from
|
|
38
|
+
// an empty `counts`. A missing body is indeterminate too — it is no evidence that nothing links here.
|
|
39
|
+
return buildDeleteReport(ids, res?.counts ?? {}, res != null && res.checked !== false)
|
|
40
|
+
} catch (e) {
|
|
41
|
+
error.value = (e as { statusMessage?: string })?.statusMessage ?? t('list.refCheckFailed')
|
|
42
|
+
throw e
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const confirmDelete = (ids: number[]) => bulk('delete', ids)
|
|
47
|
+
const duplicate = (ids: number[]) => bulk('duplicate', ids)
|
|
48
|
+
/** Persist a new status; the server's write pipeline (emitWrite) turns that into a publish/unpublish. */
|
|
49
|
+
const setStatus = (ids: number[], status: 'published' | 'draft') =>
|
|
50
|
+
bulk(status === 'published' ? 'publish' : 'unpublish', ids)
|
|
51
|
+
|
|
52
|
+
return { busy, error, previewDelete, confirmDelete, duplicate, setStatus }
|
|
53
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SerializedCollection } from '../../../core/server/utils/serialize-collection'
|
|
2
|
+
|
|
3
|
+
// Thin wrapper over the shared cached-resource loader; keeps the named `collections` return.
|
|
4
|
+
export function useCollections() {
|
|
5
|
+
const { state: collections, load } = useCachedResource<SerializedCollection>('kestrel-collections', '/api/collections')
|
|
6
|
+
return { collections, load }
|
|
7
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface ContentLocales {
|
|
2
|
+
/** Website/content locales (NOT the admin-UI language) — what the editor LocaleBar offers. */
|
|
3
|
+
locales: string[]
|
|
4
|
+
primary: string
|
|
5
|
+
/** Whether the primary locale is prefixed in public URLs too (`/en/about`); the `localePath` flag. */
|
|
6
|
+
prefixPrimary: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Resolve the configured content locales from a `runtimeConfig.public`-shaped object. Pure, so the
|
|
10
|
+
* fallback/primary logic is unit-testable without a Nuxt context. The `['en']` fallback is a safe
|
|
11
|
+
* last resort for an absent/empty config (nuxt.config always sets `public.locales`): primary-only is
|
|
12
|
+
* a safer guess than assuming a second locale exists, so it intentionally differs from the server's
|
|
13
|
+
* `['en','de']` *default* (which applies only when KESTREL_LOCALES itself is unset). */
|
|
14
|
+
export function pickContentLocales(pub: { locales?: unknown; primaryLocale?: unknown; prefixPrimary?: unknown }): ContentLocales {
|
|
15
|
+
const locales = Array.isArray(pub.locales) && pub.locales.length ? pub.locales.map(String) : ['en']
|
|
16
|
+
const primary =
|
|
17
|
+
typeof pub.primaryLocale === 'string' && locales.includes(pub.primaryLocale) ? pub.primaryLocale : locales[0]!
|
|
18
|
+
return { locales, primary, prefixPrimary: pub.prefixPrimary === true }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useContentLocales(): ContentLocales {
|
|
22
|
+
return pickContentLocales(useRuntimeConfig().public as { locales?: unknown; primaryLocale?: unknown; prefixPrimary?: unknown })
|
|
23
|
+
}
|