@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,203 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { localePath } from '../../../core/app/utils/locale-path'
|
|
3
|
+
import { resolveCollectionEditor } from '../utils/editor-registry'
|
|
4
|
+
import { editorFormContextKey } from '../utils/editor-form-context'
|
|
5
|
+
import '../utils/register-builtin-editors'
|
|
6
|
+
|
|
7
|
+
const props = withDefaults(
|
|
8
|
+
// `actions`: render the inline Save/Cancel row. The record editor hides it and drives the save from
|
|
9
|
+
// its page header (where Delete also lives) via a `type=submit` button tied to this form by `formId`;
|
|
10
|
+
// the singleton collection page keeps the inline actions. `formId` ids the form for that association.
|
|
11
|
+
defineProps<{ collection: string; id: string; localeParam?: string; group?: string; actions?: boolean; formId?: string }>(),
|
|
12
|
+
{ actions: true },
|
|
13
|
+
)
|
|
14
|
+
const emit = defineEmits<{ saved: [record: unknown]; cancel: [] }>()
|
|
15
|
+
|
|
16
|
+
const saved = ref(false)
|
|
17
|
+
const { t } = useT()
|
|
18
|
+
const toast = useToast()
|
|
19
|
+
const f = useEditForm({ collection: props.collection, id: props.id, locale: props.localeParam, group: props.group })
|
|
20
|
+
|
|
21
|
+
// Editor → field-widget context. MUST run BEFORE the top-level `await` below — after an await the setup
|
|
22
|
+
// instance context is gone and `provide` silently no-ops. `id`/`saved` let a widget clean up an abandoned
|
|
23
|
+
// never-saved draft (the secure-gallery namespace); `values` (reactive, fills after `ready`) lets it read
|
|
24
|
+
// sibling fields (the gallery slug = the proofing key). Mirrors the blockEdit provide.
|
|
25
|
+
provide(recordEditContextKey, { id: toRef(props, 'id'), saved, values: f.values })
|
|
26
|
+
|
|
27
|
+
// Editor → body context. The shell owns the form/save/history/expose; the resolved body only lays out the
|
|
28
|
+
// values. Provided (like recordEditContextKey) BEFORE the await — the computeds below are lazy, so building
|
|
29
|
+
// them against `f`'s refs before `ready` resolves is safe. `revealError` is registered by a body (the
|
|
30
|
+
// blocks body focuses the offending block) and called by `onSave` on failure, keeping the shell generic.
|
|
31
|
+
const renderable = computed(() =>
|
|
32
|
+
Object.fromEntries(Object.entries(f.renderableFields.value).map(([name, field]) => [name, asFieldDef(field)])),
|
|
33
|
+
)
|
|
34
|
+
const rootDeadFields = computed(() => deadFieldsAt(f.deadRefs.value, null))
|
|
35
|
+
// The record's own title for the header heading (blank → the host falls back to "Edit {collection} #{id}").
|
|
36
|
+
const heading = computed(() => recordTitle(renderable.value, f.values))
|
|
37
|
+
// Page/root field pane bindings — rendered by both the fields body and the blocks 3-pane root. Built once
|
|
38
|
+
// here so the two bodies differ only in layout chrome.
|
|
39
|
+
const pageFieldsBindings = computed(() => ({
|
|
40
|
+
translatable: f.translatable.value, collection: props.collection, id: props.id, mode: f.mode.value,
|
|
41
|
+
locale: f.locale.value, translations: f.translations.value, group: f.translationGroup.value,
|
|
42
|
+
fields: renderable.value, fieldLayout: f.fieldLayout.value, values: f.values, errors: f.errors, deadFields: rootDeadFields.value,
|
|
43
|
+
pageLike: f.pageLike.value, seo: f.hasSeo.value, status: f.hasStatus.value, disabled: f.saving.value,
|
|
44
|
+
}))
|
|
45
|
+
// camelCase keys: `v-on="obj"` does not camelize keys at runtime, so a kebab `copy-from` key becomes the
|
|
46
|
+
// `onCopy-from` listener — which never matches PageFieldsPane's declared `copyFrom` emit. The declared
|
|
47
|
+
// emit names are the contract.
|
|
48
|
+
const pageFieldsHandlers = { copyFrom: onCopyFrom, update: f.setField }
|
|
49
|
+
|
|
50
|
+
// Live-preview affordances (the header's "open in new tab" link, the status Ampel, the preview
|
|
51
|
+
// iframe): the current publish status and the record's real public URL (localized), or null when the
|
|
52
|
+
// record has no routable path (non-pageLike, never saved). Built from the SAVED path (baseline) — an
|
|
53
|
+
// unsaved slug edit must not point the preview/link at a URL that does not exist yet; a save moves the
|
|
54
|
+
// baseline and both follow. Defined BEFORE the provide so the blocks body can read `previewUrl` from
|
|
55
|
+
// the context (composable + lazy computeds — safe pre-`ready`).
|
|
56
|
+
const { primary, prefixPrimary } = useContentLocales()
|
|
57
|
+
const status = computed(() => (f.values.status as string | undefined) ?? '')
|
|
58
|
+
const previewUrl = computed(() => {
|
|
59
|
+
const path = f.savedPath.value
|
|
60
|
+
if (!f.pageLike.value || !path || props.id === 'new') return null
|
|
61
|
+
return localePath(path, f.locale.value, primary, prefixPrimary)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
let revealError: (() => void) | null = null
|
|
65
|
+
provide(editorFormContextKey, {
|
|
66
|
+
values: f.values, errors: f.errors, formError: f.formError, blockErrors: f.blockErrors, setField: f.setField,
|
|
67
|
+
locale: f.locale, translations: f.translations, translationGroup: f.translationGroup, deadRefs: f.deadRefs,
|
|
68
|
+
saving: f.saving, mode: f.mode, translatable: f.translatable, pageLike: f.pageLike, hasSeo: f.hasSeo,
|
|
69
|
+
hasStatus: f.hasStatus, blocksAllowed: f.blocksAllowed, renderable, previewUrl, undo: f.undo, redo: f.redo,
|
|
70
|
+
applyFrom: f.applyFrom, pageFieldsBindings, pageFieldsHandlers,
|
|
71
|
+
registerRevealError: (fn: () => void) => { revealError = fn },
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
await f.ready
|
|
75
|
+
|
|
76
|
+
const {
|
|
77
|
+
formError, saving, submit, dirty, editorType, pageLike, hasStatus,
|
|
78
|
+
savedStatus, undo, redo, canUndo, canRedo,
|
|
79
|
+
} = f
|
|
80
|
+
|
|
81
|
+
// Which editor body renders (fields · blocks · an extension type). Resolved from the registry; an
|
|
82
|
+
// unregistered type falls back to the visible EditorUnsupported panel.
|
|
83
|
+
const bodyComponent = computed(() => resolveCollectionEditor(editorType.value))
|
|
84
|
+
|
|
85
|
+
// Right dot of the editor Ampel: the live / generated state of THIS record's static page. Only a saved
|
|
86
|
+
// pageLike record has one (a non-pageLike collection produces no static page; an unsaved `new` has no row).
|
|
87
|
+
// Refreshed on mount and after each save (a save may (re)publish the page).
|
|
88
|
+
const liveStatus = usePublishStatus({
|
|
89
|
+
collection: () => props.collection,
|
|
90
|
+
id: () => props.id,
|
|
91
|
+
locale: () => f.locale.value,
|
|
92
|
+
enabled: () => pageLike.value && props.id !== 'new',
|
|
93
|
+
})
|
|
94
|
+
// Carries "poll the live status on mount" across the create→navigate remount (see usePendingPublishPoll).
|
|
95
|
+
const pendingPoll = usePendingPublishPoll()
|
|
96
|
+
|
|
97
|
+
// The record-editor page renders the action toolbar (Save/Cancel/Delete + Undo/Redo) in its header: it
|
|
98
|
+
// submits this form by id (formId), reads the in-flight `saving` for the button state, drives the
|
|
99
|
+
// unsaved-changes guard off `dirty`, and the undo/redo controls off the history API exposed here.
|
|
100
|
+
// The exposed shape IS the `EditorExpose` contract (utils/editor-expose.ts) — keep them in sync.
|
|
101
|
+
defineExpose({ dirty, saving, undo, redo, canUndo, canRedo, hasStatus, status, savedStatus, previewUrl, pageLike, live: liveStatus.data, recordTitle: heading })
|
|
102
|
+
|
|
103
|
+
async function onSave() {
|
|
104
|
+
const r = await submit()
|
|
105
|
+
if (r.ok) {
|
|
106
|
+
saved.value = true
|
|
107
|
+
// The save may have (re)published the page. A PUBLISHED page republishes asynchronously (a debounced
|
|
108
|
+
// queue) in prod, so poll the right lamp until it settles (Live / Error) rather than catching only the
|
|
109
|
+
// in-flight state; `since` is the pre-save row timestamp so a stale prior success/error can't settle the
|
|
110
|
+
// poll early. A DRAFT produces no file → a single refresh (to reflect the draft / cleared row).
|
|
111
|
+
const wasNew = props.id === 'new'
|
|
112
|
+
const isDraft = hasStatus.value && status.value === 'draft'
|
|
113
|
+
const since = liveStatus.data.value.updatedAt ?? null
|
|
114
|
+
// A create navigates the editor to the new record's URL (a full remount), so this `new` instance is
|
|
115
|
+
// torn down before its poll could run — hand the intent to the arriving instance instead.
|
|
116
|
+
if (wasNew) pendingPoll.value = pageLike.value && !isDraft
|
|
117
|
+
emit('saved', r.record)
|
|
118
|
+
if (!wasNew) void (isDraft ? liveStatus.refresh() : liveStatus.refreshUntilSettled({ since }))
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
// Save failed — let the active body reveal the problem (the blocks body focuses the offending block /
|
|
122
|
+
// deselects to show the page fields); then guarantee the failure is noticed on a long editor.
|
|
123
|
+
revealError?.()
|
|
124
|
+
toast.error(formError.value || t('editor.saveFailed'))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// LocaleBar "copy" — pull a sibling locale's content into the current form (multi only; the button is
|
|
128
|
+
// shown only for an existing sibling, so its id is in the translations map).
|
|
129
|
+
async function onCopyFrom(loc: string) {
|
|
130
|
+
const tid = f.translations.value[loc]
|
|
131
|
+
if (typeof tid !== 'number') return
|
|
132
|
+
try {
|
|
133
|
+
const row = await $fetch<Record<string, unknown>>(`/api/${props.collection}/${tid}`)
|
|
134
|
+
f.applyFrom(row)
|
|
135
|
+
} catch {
|
|
136
|
+
f.formError.value = t('localeBar.copyFailed', { locale: loc.toUpperCase() })
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Native unload (tab close / reload / external navigation) bypasses the in-app route guard on the editor
|
|
141
|
+
// pages, so a beforeunload prompt guards the same unsaved changes. Lives here (where `dirty` is owned) so
|
|
142
|
+
// both the record editor and the singleton editor are covered.
|
|
143
|
+
function onBeforeUnload(e: BeforeUnloadEvent) {
|
|
144
|
+
if (dirty.value) {
|
|
145
|
+
e.preventDefault()
|
|
146
|
+
e.returnValue = '' // legacy fallback for the native prompt
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
onMounted(() => {
|
|
150
|
+
window.addEventListener('beforeunload', onBeforeUnload)
|
|
151
|
+
if (pendingPoll.value) {
|
|
152
|
+
// Arrived here from a create → the republish is still in flight, so poll until the right lamp settles.
|
|
153
|
+
pendingPoll.value = false
|
|
154
|
+
void liveStatus.refreshUntilSettled()
|
|
155
|
+
} else {
|
|
156
|
+
void liveStatus.refresh() // fetch the live/generated state once the saved record (path/locale) is known
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
onUnmounted(() => {
|
|
160
|
+
window.removeEventListener('beforeunload', onBeforeUnload)
|
|
161
|
+
})
|
|
162
|
+
</script>
|
|
163
|
+
|
|
164
|
+
<template>
|
|
165
|
+
<!-- novalidate: Zod owns validation (inline + banner). Native constraint validation would run BEFORE
|
|
166
|
+
the submit event and block onSave with a browser bubble — wrongly so on proxy inputs whose text
|
|
167
|
+
doesn't mirror the model (combobox search boxes stay empty although records are selected). -->
|
|
168
|
+
<form :id="formId" class="editor" novalidate @submit.prevent="onSave">
|
|
169
|
+
<p v-if="formError" class="editor__error" role="alert">{{ formError }}</p>
|
|
170
|
+
|
|
171
|
+
<!-- The editor body is chosen by the collection's `editor` type (fields · blocks · an extension). -->
|
|
172
|
+
<component :is="bodyComponent" v-if="bodyComponent" />
|
|
173
|
+
<EditorUnsupported v-else :editor="editorType" />
|
|
174
|
+
|
|
175
|
+
<div v-if="actions" class="editor__actions">
|
|
176
|
+
<UiButton type="submit" variant="primary" icon="check" :loading="saving">{{ t('common.save') }}</UiButton>
|
|
177
|
+
<UiButton type="button" variant="secondary" icon="x" :disabled="saving" @click="emit('cancel')">{{ t('common.cancel') }}</UiButton>
|
|
178
|
+
</div>
|
|
179
|
+
</form>
|
|
180
|
+
</template>
|
|
181
|
+
|
|
182
|
+
<style lang="scss">
|
|
183
|
+
// The editor form fills the height handed down by the page so its body/panes can own the scroll.
|
|
184
|
+
.editor {
|
|
185
|
+
display: flex;
|
|
186
|
+
flex-direction: column;
|
|
187
|
+
gap: var(--space-5);
|
|
188
|
+
flex: 1 1 auto;
|
|
189
|
+
min-height: 0;
|
|
190
|
+
|
|
191
|
+
&__error {
|
|
192
|
+
padding: var(--space-3) var(--space-4);
|
|
193
|
+
border: 1px solid var(--color-danger);
|
|
194
|
+
border-radius: var(--radius-sm);
|
|
195
|
+
color: var(--color-danger);
|
|
196
|
+
font-size: var(--text-sm);
|
|
197
|
+
}
|
|
198
|
+
&__actions {
|
|
199
|
+
display: flex;
|
|
200
|
+
gap: var(--space-3);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
</style>
|