@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,401 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, inject, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
3
|
+
import type { SerializedBlock } from '../../../core/server/utils/serialize-collection'
|
|
4
|
+
import { previewSrc, parseFrameMessage, type PreviewBlockNode } from '../../../public/app/utils/preview-protocol'
|
|
5
|
+
import { createPreviewSender, acceptsFrameEvent, type PreviewSender } from '../utils/preview-channel'
|
|
6
|
+
import { editorFormContextKey } from '../utils/editor-form-context'
|
|
7
|
+
import { PRESETS, matchPreset, fitScale, resolveDim, clampDim, DIM_MIN, WIDTH_MAX, HEIGHT_MAX, type ViewportPreset } from '../utils/preview-viewport'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The editor half of the live preview: an iframe onto the REAL public page (the record's URL for a
|
|
11
|
+
* saved pageLike record — real CSS, fonts, breakpoints, drafts included via the same-origin admin
|
|
12
|
+
* session — else the dedicated fallback page), kept keystroke-live over the postMessage bridge. The
|
|
13
|
+
* populated tree is pushed to the iframe rAF-coalesced (≤ one frame behind typing, no debounce). Selection syncs both
|
|
14
|
+
* ways: tree click → highlight/scroll in the frame; block click in the frame → `select` up to the tree.
|
|
15
|
+
*/
|
|
16
|
+
const props = defineProps<{ content: unknown[]; locale?: string; selectedId?: string | null }>()
|
|
17
|
+
const emit = defineEmits<{ select: [id: string] }>()
|
|
18
|
+
const { t } = useT()
|
|
19
|
+
|
|
20
|
+
// The record's public URL comes from the editor shell (null → fallback page). Injected optionally so
|
|
21
|
+
// the component still mounts standalone (tests, storybook-ish usage) — it then always uses the fallback.
|
|
22
|
+
const ctx = inject(editorFormContextKey, null)
|
|
23
|
+
const src = computed(() => previewSrc(ctx?.previewUrl.value ?? null, props.locale ?? ''))
|
|
24
|
+
|
|
25
|
+
// ---- populate (pipeline: resolve media ids + internal links into reactive caches) ----
|
|
26
|
+
// Block field schemas (non-blocking: byType fills once loaded, then the next send carries the media).
|
|
27
|
+
const { blocks: blockDefs, load } = useBlocks()
|
|
28
|
+
load()
|
|
29
|
+
const byType = computed<Record<string, SerializedBlock>>(() =>
|
|
30
|
+
Object.fromEntries((blockDefs.value ?? []).map((b) => [b.name, b])),
|
|
31
|
+
)
|
|
32
|
+
const items = computed(() => (Array.isArray(props.content) ? props.content : []))
|
|
33
|
+
// `locale` is a getter so a locale change re-resolves media (its cache is per-locale).
|
|
34
|
+
const { ensure: ensureMedia, resolve: resolveMedia } = useMediaResolver(() => props.locale ?? 'en')
|
|
35
|
+
const { ensure: ensureLinks, resolve: resolveLink } = useLinkResolver()
|
|
36
|
+
// The current resolve round. The ready-handshake push awaits it: the frame's first paint is the
|
|
37
|
+
// SERVER-populated page, so pushing a not-yet-resolved client tree would momentarily strip its images/
|
|
38
|
+
// hrefs. With warm (or empty) caches the promise settles immediately — no perceptible delay.
|
|
39
|
+
let ensureRound: Promise<unknown> = Promise.resolve()
|
|
40
|
+
watch(
|
|
41
|
+
[items, byType, () => props.locale],
|
|
42
|
+
([list, types]) => {
|
|
43
|
+
ensureRound = Promise.allSettled([
|
|
44
|
+
ensureMedia(collectMediaIds(list, types)),
|
|
45
|
+
ensureLinks(collectLinkRefs(list, types)),
|
|
46
|
+
])
|
|
47
|
+
},
|
|
48
|
+
{ immediate: true, deep: true },
|
|
49
|
+
)
|
|
50
|
+
const populated = computed(() =>
|
|
51
|
+
populateBlocksLinks(populateBlocksMedia(items.value, byType.value, resolveMedia), byType.value, resolveLink),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
// ---- channel (parent side) ----
|
|
55
|
+
const frame = ref<HTMLIFrameElement | null>(null)
|
|
56
|
+
// True once the CURRENT frame document said hello; sends before that are pointless (lost on load).
|
|
57
|
+
const ready = ref(false)
|
|
58
|
+
let sender: PreviewSender | null = null
|
|
59
|
+
|
|
60
|
+
function pushContent() {
|
|
61
|
+
sender?.sendContent(populated.value as PreviewBlockNode[])
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function onMessage(e: MessageEvent) {
|
|
65
|
+
if (!acceptsFrameEvent(e, frame.value?.contentWindow ?? null, window.location.origin)) return
|
|
66
|
+
const m = parseFrameMessage(e.data)
|
|
67
|
+
if (!m) return
|
|
68
|
+
if (m.kestrel === 'preview:ready') {
|
|
69
|
+
// Fresh document (first load OR reload/src change) — send it the full current state, once the
|
|
70
|
+
// in-flight media/link resolve settles (see ensureRound); `ready` may flip meanwhile (reload).
|
|
71
|
+
ready.value = true
|
|
72
|
+
void ensureRound.then(() => { if (ready.value) pushContent() })
|
|
73
|
+
} else {
|
|
74
|
+
emit('select', m.id)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Every load starts a document that must handshake before it may receive pushes. If consumer script
|
|
79
|
+
// navigated the canvas away (programmatic router push — clicks/submits are already intercepted in the
|
|
80
|
+
// bridge), the loaded document is not the preview page: snap the frame back to the preview URL.
|
|
81
|
+
function onFrameLoad() {
|
|
82
|
+
ready.value = false
|
|
83
|
+
const el = frame.value
|
|
84
|
+
if (!el) return
|
|
85
|
+
try {
|
|
86
|
+
const loc = el.contentWindow?.location
|
|
87
|
+
if (loc && loc.pathname + loc.search !== src.value) el.src = src.value
|
|
88
|
+
} catch {
|
|
89
|
+
el.src = src.value // cross-origin navigation — location unreadable, snap back
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Manual fallback for any residual stale component state: reload the iframe. This remounts EVERY
|
|
94
|
+
// component in the frame (not just, say, a stale srcset) and re-handshakes — the fresh document posts
|
|
95
|
+
// `preview:ready`, and onMessage re-pushes the full populated tree after ensureRound. `ready=false`
|
|
96
|
+
// guards pushes to the doc that's tearing down.
|
|
97
|
+
function refresh() {
|
|
98
|
+
ready.value = false
|
|
99
|
+
const el = frame.value
|
|
100
|
+
if (!el) return
|
|
101
|
+
try {
|
|
102
|
+
el.contentWindow?.location.reload() // same URL → onFrameLoad's snap-back check passes
|
|
103
|
+
} catch {
|
|
104
|
+
el.src = src.value // cross-origin: location unreadable → renavigate
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
onMounted(() => {
|
|
109
|
+
sender = createPreviewSender(() => frame.value?.contentWindow ?? null, window.location.origin, () => props.selectedId ?? null)
|
|
110
|
+
window.addEventListener('message', onMessage)
|
|
111
|
+
})
|
|
112
|
+
onUnmounted(() => {
|
|
113
|
+
sender?.dispose()
|
|
114
|
+
sender = null
|
|
115
|
+
window.removeEventListener('message', onMessage)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// Keystroke-live: every populated-tree change (edit, media/link cache fill) schedules a send; the
|
|
119
|
+
// channel coalesces bursts to one message per animation frame. Selection posts immediately (tiny).
|
|
120
|
+
watch(populated, () => { if (ready.value) pushContent() })
|
|
121
|
+
watch(() => props.selectedId, (id) => { if (ready.value) sender?.sendSelected(id ?? null) })
|
|
122
|
+
// A src change reloads the iframe (locale switch, first save assigning a URL) → wait for its new ready.
|
|
123
|
+
watch(src, () => { ready.value = false })
|
|
124
|
+
|
|
125
|
+
// ---- responsive preview: device presets (quick-fill) + automatic scale-to-fit + custom W×H ----
|
|
126
|
+
// The iframe renders at the target resolution's REAL px (so the page fires its true breakpoints); a
|
|
127
|
+
// uniform transform only shrinks it visually to fit the pane. Desktop keeps the config-driven breakpoint
|
|
128
|
+
// width but an 'auto' height that fills the pane vertically (the iframe's own document scrolls).
|
|
129
|
+
const previewDesktopWidth = Number((useRuntimeConfig().public as { previewDesktopWidth?: number }).previewDesktopWidth) || 1440
|
|
130
|
+
const presets = PRESETS(previewDesktopWidth)
|
|
131
|
+
const { width, height } = usePreviewViewport(previewDesktopWidth)
|
|
132
|
+
const activePreset = computed(() => matchPreset(width.value, height.value, presets))
|
|
133
|
+
|
|
134
|
+
function selectPreset(p: ViewportPreset) {
|
|
135
|
+
width.value = p.w
|
|
136
|
+
height.value = p.h
|
|
137
|
+
}
|
|
138
|
+
// Commit a W×H input on change (blur/Enter — `change` falls through to the primitive's root <input>), not
|
|
139
|
+
// per keystroke, so the frame doesn't thrash mid-typing. Empty → keep the current value; else full clamp.
|
|
140
|
+
function onDimCommit(target: 'w' | 'h', e: Event) {
|
|
141
|
+
const el = e.target as HTMLInputElement
|
|
142
|
+
if (el.value === '') {
|
|
143
|
+
// A number input also reports '' while it holds un-parsable editing text ('-', '1e'); that must keep
|
|
144
|
+
// the current value, not silently switch the axis. A genuinely EMPTIED field means "auto": the axis
|
|
145
|
+
// fills the pane. (Clearing height under the fixed desktop width re-selects the Desktop preset.)
|
|
146
|
+
if (el.validity?.badInput) return
|
|
147
|
+
if (target === 'w') width.value = 'auto'
|
|
148
|
+
else height.value = 'auto'
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
const v = clampDim(Number(el.value), DIM_MIN, target === 'w' ? WIDTH_MAX : HEIGHT_MAX)
|
|
152
|
+
if (v === null) return
|
|
153
|
+
if (target === 'w') width.value = v
|
|
154
|
+
else height.value = v
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Both stage axes drive the fit scale — measured with a ResizeObserver (VueUse isn't wired; a manual
|
|
158
|
+
// observer is the in-repo idiom, cf. useTheme's matchMedia). `scale` stays 1 until the first measure.
|
|
159
|
+
const stage = ref<HTMLElement | null>(null)
|
|
160
|
+
const availW = ref(0)
|
|
161
|
+
const availH = ref(0)
|
|
162
|
+
let ro: ResizeObserver | null = null
|
|
163
|
+
const scale = computed(() => fitScale(availW.value, availH.value, width.value, height.value))
|
|
164
|
+
const scalePct = computed(() => Math.round(scale.value * 100))
|
|
165
|
+
// Resolve each Dim to real px: fixed passes through, an 'auto' axis fills the pane at the current scale
|
|
166
|
+
// (falls back to the config width / 900 before measurement so SSR/tests still have a sized frame).
|
|
167
|
+
const frameW = computed(() => resolveDim(availW.value, width.value, scale.value, previewDesktopWidth))
|
|
168
|
+
const frameH = computed(() => resolveDim(availH.value, height.value, scale.value, 900))
|
|
169
|
+
|
|
170
|
+
// The iframe keeps its real target px and is scaled from its top-left; the holder occupies the SCALED
|
|
171
|
+
// footprint so centering behaves. The stage clips (overflow:hidden) so only the iframe document scrolls.
|
|
172
|
+
const frameStyle = computed(() => ({
|
|
173
|
+
width: `${frameW.value}px`,
|
|
174
|
+
height: `${frameH.value}px`,
|
|
175
|
+
transform: `scale(${scale.value})`,
|
|
176
|
+
transformOrigin: 'top left',
|
|
177
|
+
}))
|
|
178
|
+
const viewportStyle = computed(() => ({
|
|
179
|
+
width: `${frameW.value * scale.value}px`,
|
|
180
|
+
height: `${frameH.value * scale.value}px`,
|
|
181
|
+
}))
|
|
182
|
+
|
|
183
|
+
// Content box (padding excluded) so the initial seed matches the ResizeObserver's `contentRect` — both
|
|
184
|
+
// must agree, else the frame footprint would briefly exceed the padded stage (clipped, not scrolled).
|
|
185
|
+
function contentBox(el: HTMLElement) {
|
|
186
|
+
const cs = getComputedStyle(el)
|
|
187
|
+
return {
|
|
188
|
+
width: el.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight),
|
|
189
|
+
height: el.clientHeight - parseFloat(cs.paddingTop) - parseFloat(cs.paddingBottom),
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
onMounted(() => {
|
|
193
|
+
const el = stage.value
|
|
194
|
+
if (!el || typeof ResizeObserver === 'undefined') return
|
|
195
|
+
ro = new ResizeObserver((entries) => {
|
|
196
|
+
const b = entries[0]?.contentRect ?? contentBox(el)
|
|
197
|
+
availW.value = b.width
|
|
198
|
+
availH.value = b.height
|
|
199
|
+
})
|
|
200
|
+
ro.observe(el)
|
|
201
|
+
const b = contentBox(el)
|
|
202
|
+
availW.value = b.width
|
|
203
|
+
availH.value = b.height
|
|
204
|
+
})
|
|
205
|
+
onUnmounted(() => { ro?.disconnect(); ro = null })
|
|
206
|
+
</script>
|
|
207
|
+
|
|
208
|
+
<template>
|
|
209
|
+
<section class="block-preview" :aria-label="t('preview.ariaLabel')">
|
|
210
|
+
<div class="block-preview__bar">
|
|
211
|
+
<p class="block-preview__label">{{ t('preview.label') }}</p>
|
|
212
|
+
|
|
213
|
+
<div class="block-preview__tools">
|
|
214
|
+
<div class="block-preview__group">
|
|
215
|
+
<UiTooltip>
|
|
216
|
+
<UiButton
|
|
217
|
+
type="button"
|
|
218
|
+
variant="ghost"
|
|
219
|
+
size="sm"
|
|
220
|
+
icon="rotate-cw"
|
|
221
|
+
:aria-label="t('preview.refresh')"
|
|
222
|
+
@click="refresh"
|
|
223
|
+
/>
|
|
224
|
+
<template #content>{{ t('preview.refresh') }}</template>
|
|
225
|
+
</UiTooltip>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<span class="block-preview__sep" aria-hidden="true" />
|
|
229
|
+
|
|
230
|
+
<div class="block-preview__group" role="group" :aria-label="t('preview.deviceLabel')">
|
|
231
|
+
<UiTooltip v-for="p in presets" :key="p.key">
|
|
232
|
+
<UiButton
|
|
233
|
+
type="button"
|
|
234
|
+
variant="ghost"
|
|
235
|
+
size="sm"
|
|
236
|
+
:icon="p.icon"
|
|
237
|
+
:aria-label="t(p.label)"
|
|
238
|
+
:aria-pressed="activePreset === p.key"
|
|
239
|
+
:class="{ 'block-preview__seg--active': activePreset === p.key }"
|
|
240
|
+
@click="selectPreset(p)"
|
|
241
|
+
/>
|
|
242
|
+
<template #content>{{ t(p.label) }} · {{ p.w }} × {{ p.h === 'auto' ? t('preview.auto') : p.h }}</template>
|
|
243
|
+
</UiTooltip>
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
<span class="block-preview__sep" aria-hidden="true" />
|
|
247
|
+
|
|
248
|
+
<!-- Read-only: scale is always automatic (two-axis fit), so the % is an indicator, not a control. -->
|
|
249
|
+
<span class="block-preview__pct" aria-hidden="true">{{ scalePct }}%</span>
|
|
250
|
+
|
|
251
|
+
<span class="block-preview__sep" aria-hidden="true" />
|
|
252
|
+
|
|
253
|
+
<div class="block-preview__dims">
|
|
254
|
+
<!-- Either axis can be 'Auto' (fills the pane): typing a number fixes it, clearing the field
|
|
255
|
+
returns it to Auto. Height is Auto under the Desktop preset. -->
|
|
256
|
+
<UiNumberInput
|
|
257
|
+
slim
|
|
258
|
+
class="block-preview__dim"
|
|
259
|
+
:model-value="width === 'auto' ? null : width"
|
|
260
|
+
:min="DIM_MIN"
|
|
261
|
+
:max="WIDTH_MAX"
|
|
262
|
+
:placeholder="width === 'auto' ? t('preview.auto') : undefined"
|
|
263
|
+
:aria-label="t('preview.width')"
|
|
264
|
+
@change="onDimCommit('w', $event)"
|
|
265
|
+
/>
|
|
266
|
+
<span class="block-preview__x" aria-hidden="true">×</span>
|
|
267
|
+
<UiNumberInput
|
|
268
|
+
slim
|
|
269
|
+
class="block-preview__dim"
|
|
270
|
+
:model-value="height === 'auto' ? null : height"
|
|
271
|
+
:min="DIM_MIN"
|
|
272
|
+
:max="HEIGHT_MAX"
|
|
273
|
+
:placeholder="height === 'auto' ? t('preview.auto') : undefined"
|
|
274
|
+
:aria-label="t('preview.height')"
|
|
275
|
+
@change="onDimCommit('h', $event)"
|
|
276
|
+
/>
|
|
277
|
+
<span class="block-preview__unit" aria-hidden="true">px</span>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
<div ref="stage" class="block-preview__stage">
|
|
283
|
+
<div class="block-preview__viewport" :style="viewportStyle">
|
|
284
|
+
<iframe ref="frame" class="block-preview__frame" :src="src" :style="frameStyle" :title="t('preview.label')" @load="onFrameLoad" />
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
</section>
|
|
288
|
+
</template>
|
|
289
|
+
|
|
290
|
+
<style lang="scss">
|
|
291
|
+
// The iframe owns rendering + scrolling; this host only frames it and hosts the device toggle.
|
|
292
|
+
.block-preview {
|
|
293
|
+
flex: 1 1 auto;
|
|
294
|
+
min-height: 0;
|
|
295
|
+
display: flex;
|
|
296
|
+
flex-direction: column;
|
|
297
|
+
gap: var(--space-2);
|
|
298
|
+
|
|
299
|
+
&__bar {
|
|
300
|
+
display: flex;
|
|
301
|
+
flex-wrap: wrap;
|
|
302
|
+
align-items: center;
|
|
303
|
+
justify-content: space-between;
|
|
304
|
+
gap: var(--space-2);
|
|
305
|
+
flex: 0 0 auto;
|
|
306
|
+
}
|
|
307
|
+
// Pane heading, folded onto the toolbar row so the preview header is a single thin line (the outer
|
|
308
|
+
// editor pane's own "Preview" label is hidden below to avoid a duplicate).
|
|
309
|
+
&__label {
|
|
310
|
+
margin: 0;
|
|
311
|
+
font-size: var(--text-sm);
|
|
312
|
+
font-weight: var(--weight-medium);
|
|
313
|
+
color: var(--color-text-muted);
|
|
314
|
+
flex: 0 0 auto;
|
|
315
|
+
}
|
|
316
|
+
// Controls cluster to the right of the heading, wrapping among themselves; the whole cluster only
|
|
317
|
+
// wraps below the heading when the middle pane is too narrow to fit both.
|
|
318
|
+
&__tools {
|
|
319
|
+
display: flex;
|
|
320
|
+
flex-wrap: wrap;
|
|
321
|
+
align-items: center;
|
|
322
|
+
justify-content: flex-end;
|
|
323
|
+
gap: var(--space-2);
|
|
324
|
+
flex: 1 1 auto;
|
|
325
|
+
min-width: 0;
|
|
326
|
+
}
|
|
327
|
+
&__group {
|
|
328
|
+
display: flex;
|
|
329
|
+
align-items: center;
|
|
330
|
+
gap: var(--space-1);
|
|
331
|
+
}
|
|
332
|
+
// Active segment: neutral fill + indigo glyph (matches the button-group active idiom).
|
|
333
|
+
&__seg--active {
|
|
334
|
+
color: var(--color-primary);
|
|
335
|
+
background: var(--color-surface-2);
|
|
336
|
+
}
|
|
337
|
+
&__sep {
|
|
338
|
+
align-self: stretch;
|
|
339
|
+
width: 1px;
|
|
340
|
+
min-height: 1.25rem;
|
|
341
|
+
background: var(--color-border);
|
|
342
|
+
}
|
|
343
|
+
&__pct {
|
|
344
|
+
min-width: 3.5ch;
|
|
345
|
+
text-align: right;
|
|
346
|
+
font-size: var(--text-sm);
|
|
347
|
+
color: var(--color-text-muted);
|
|
348
|
+
font-variant-numeric: tabular-nums;
|
|
349
|
+
}
|
|
350
|
+
&__dims {
|
|
351
|
+
display: flex;
|
|
352
|
+
align-items: center;
|
|
353
|
+
gap: var(--space-1);
|
|
354
|
+
font-size: var(--text-sm);
|
|
355
|
+
color: var(--color-text-muted);
|
|
356
|
+
}
|
|
357
|
+
// Compact numeric inputs (the primitive is width:100% by default — override under our wrapper so the
|
|
358
|
+
// selector wins by specificity without touching the shared component). 6rem fits up to 5 typed digits
|
|
359
|
+
// without clipping; flex:none stops the narrow middle pane from shrinking them back below that width.
|
|
360
|
+
&__dims .ui-number {
|
|
361
|
+
width: 6rem;
|
|
362
|
+
flex: none;
|
|
363
|
+
font-variant-numeric: tabular-nums;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
&__stage {
|
|
367
|
+
flex: 1 1 auto;
|
|
368
|
+
min-height: 0;
|
|
369
|
+
display: flex;
|
|
370
|
+
justify-content: center;
|
|
371
|
+
align-items: center;
|
|
372
|
+
// The container NEVER scrolls: the frame is always scaled to fit both axes, and any over-height
|
|
373
|
+
// content scrolls inside the iframe's own document. Center the (possibly letterboxed) device frame.
|
|
374
|
+
overflow: hidden;
|
|
375
|
+
// No padding: the pane already has its own, and an inset here would misalign the frame's top corners
|
|
376
|
+
// with the rest of the editor's content edge.
|
|
377
|
+
|
|
378
|
+
// Stacked single-column editor (matches the editor3 breakpoint in BlocksBody): the panes become
|
|
379
|
+
// content-sized there, so give the stage a concrete height to measure + scale the frame within.
|
|
380
|
+
@media (max-width: 1100px) {
|
|
381
|
+
height: 70vh;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// Occupies the SCALED footprint of the frame so centering + overflow behave; owns the crisp 1px border
|
|
385
|
+
// (box-shadow, unscaled). Deliberately square: the preview must read as the RAW page, not a device mock,
|
|
386
|
+
// so no corner radius rounds off the rendered edges. `overflow: hidden` still guards subpixel spill.
|
|
387
|
+
&__viewport {
|
|
388
|
+
flex: 0 0 auto;
|
|
389
|
+
box-shadow: 0 0 0 1px var(--color-border);
|
|
390
|
+
background: #fff;
|
|
391
|
+
overflow: hidden;
|
|
392
|
+
transition: width 160ms ease, height 160ms ease;
|
|
393
|
+
}
|
|
394
|
+
&__frame {
|
|
395
|
+
display: block;
|
|
396
|
+
border: 0;
|
|
397
|
+
background: #fff;
|
|
398
|
+
transition: transform 160ms ease;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
</style>
|