@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,147 @@
|
|
|
1
|
+
export type IconName =
|
|
2
|
+
| 'grip' | 'chevron-up' | 'chevron-down' | 'chevron-left' | 'chevron-right' | 'arrow-left'
|
|
3
|
+
| 'copy' | 'trash' | 'x' | 'plus' | 'pencil' | 'check'
|
|
4
|
+
| 'home' | 'file-text' | 'folder' | 'folder-plus' | 'panel-left-close' | 'panel-left-open' | 'log-out'
|
|
5
|
+
| 'sun' | 'moon' | 'calendar' | 'upload' | 'layout-grid' | 'table'
|
|
6
|
+
// collection icons + list toolbar
|
|
7
|
+
| 'settings' | 'image' | 'newspaper' | 'columns' | 'filter' | 'search' | 'sliders'
|
|
8
|
+
// richtext toolbar
|
|
9
|
+
| 'bold' | 'italic' | 'underline' | 'strikethrough' | 'code' | 'square-code' | 'highlighter'
|
|
10
|
+
| 'subscript' | 'superscript' | 'list' | 'list-ordered' | 'quote' | 'minus' | 'message-square'
|
|
11
|
+
| 'align-left' | 'align-center' | 'align-right' | 'align-justify'
|
|
12
|
+
| 'eraser' | 'undo' | 'redo' | 'link' | 'link-2'
|
|
13
|
+
| 'external-link' | 'triangle-alert'
|
|
14
|
+
// password reveal
|
|
15
|
+
| 'eye' | 'eye-off'
|
|
16
|
+
// preview device widths + reload
|
|
17
|
+
| 'monitor' | 'tablet' | 'smartphone' | 'rotate-cw'
|
|
18
|
+
// block-type tiles
|
|
19
|
+
| 'clock' | 'user'
|
|
20
|
+
|
|
21
|
+
export const icons: Record<IconName, string> = {
|
|
22
|
+
'monitor': '<rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/>',
|
|
23
|
+
'tablet': '<rect width="16" height="20" x="4" y="2" rx="2" ry="2"/><line x1="12" x2="12.01" y1="18" y2="18"/>',
|
|
24
|
+
'smartphone': '<rect width="14" height="20" x="5" y="2" rx="2" ry="2"/><path d="M12 18h.01"/>',
|
|
25
|
+
'rotate-cw': '<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/>',
|
|
26
|
+
'clock': '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
|
|
27
|
+
'user': '<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>',
|
|
28
|
+
'chevron-up': '<path d="m18 15-6-6-6 6"/>',
|
|
29
|
+
'chevron-down': '<path d="m6 9 6 6 6-6"/>',
|
|
30
|
+
'chevron-left': '<path d="m15 18-6-6 6-6"/>',
|
|
31
|
+
'chevron-right': '<path d="m9 18 6-6-6-6"/>',
|
|
32
|
+
'arrow-left': '<path d="m12 19-7-7 7-7"/><path d="M19 12H5"/>',
|
|
33
|
+
'x': '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
|
|
34
|
+
'plus': '<path d="M5 12h14"/><path d="M12 5v14"/>',
|
|
35
|
+
'check': '<path d="M20 6 9 17l-5-5"/>',
|
|
36
|
+
'trash': '<path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/>',
|
|
37
|
+
'copy': '<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>',
|
|
38
|
+
'grip': '<circle cx="9" cy="6" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="9" cy="18" r="1"/><circle cx="15" cy="6" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="18" r="1"/>',
|
|
39
|
+
'pencil': '<path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/>',
|
|
40
|
+
'home': '<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/>',
|
|
41
|
+
'file-text': '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/>',
|
|
42
|
+
'folder': '<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/>',
|
|
43
|
+
'panel-left-close': '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="m16 15-3-3 3-3"/>',
|
|
44
|
+
'panel-left-open': '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="m14 9 3 3-3 3"/>',
|
|
45
|
+
'log-out': '<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="m16 17 5-5-5-5"/><path d="M21 12H9"/>',
|
|
46
|
+
'sun': '<circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/>',
|
|
47
|
+
'moon': '<path d="M12 3a6.36 6.36 0 0 0 9 9 9 9 0 1 1-9-9Z"/>',
|
|
48
|
+
'calendar': '<path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/>',
|
|
49
|
+
'folder-plus': '<path d="M12 10v6"/><path d="M9 13h6"/><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/>',
|
|
50
|
+
'upload': '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" x2="12" y1="3" y2="15"/>',
|
|
51
|
+
'layout-grid': '<rect width="7" height="7" x="3" y="3" rx="1"/><rect width="7" height="7" x="14" y="3" rx="1"/><rect width="7" height="7" x="14" y="14" rx="1"/><rect width="7" height="7" x="3" y="14" rx="1"/>',
|
|
52
|
+
'table': '<path d="M12 3v18"/><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M3 15h18"/>',
|
|
53
|
+
'bold': '<path d="M14 12a4 4 0 0 0 0-8H6v8"/><path d="M15 20a4 4 0 0 0 0-8H6v8Z"/>',
|
|
54
|
+
'italic': '<line x1="19" x2="10" y1="4" y2="4"/><line x1="14" x2="5" y1="20" y2="20"/><line x1="15" x2="9" y1="4" y2="20"/>',
|
|
55
|
+
'underline': '<path d="M6 4v6a6 6 0 0 0 12 0V4"/><line x1="4" x2="20" y1="21" y2="21"/>',
|
|
56
|
+
'strikethrough': '<path d="M16 4H9a3 3 0 0 0-2.83 4"/><path d="M14 12a4 4 0 0 1 0 8H6"/><line x1="4" x2="20" y1="12" y2="12"/>',
|
|
57
|
+
'code': '<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>',
|
|
58
|
+
'square-code': '<path d="m10 9-3 3 3 3"/><path d="m14 15 3-3-3-3"/><rect width="18" height="18" x="3" y="3" rx="2"/>',
|
|
59
|
+
'highlighter': '<path d="m9 11-6 6v3h9l3-3"/><path d="m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4"/>',
|
|
60
|
+
'subscript': '<path d="m4 5 8 8"/><path d="m12 5-8 8"/><path d="M20 19h-4c0-1.5.44-2 1.5-2.5S20 15.33 20 14c0-.47-.17-.93-.48-1.29a2.11 2.11 0 0 0-2.62-.44c-.42.24-.74.62-.9 1.07"/>',
|
|
61
|
+
'superscript': '<path d="m4 19 8-8"/><path d="m12 19-8-8"/><path d="M20 12h-4c0-1.5.44-2 1.5-2.5S20 8.33 20 7c0-.47-.17-.93-.48-1.29a2.11 2.11 0 0 0-2.62-.44c-.42.24-.74.62-.9 1.07"/>',
|
|
62
|
+
'list': '<line x1="8" x2="21" y1="6" y2="6"/><line x1="8" x2="21" y1="12" y2="12"/><line x1="8" x2="21" y1="18" y2="18"/><line x1="3" x2="3.01" y1="6" y2="6"/><line x1="3" x2="3.01" y1="12" y2="12"/><line x1="3" x2="3.01" y1="18" y2="18"/>',
|
|
63
|
+
'list-ordered': '<line x1="10" x2="21" y1="6" y2="6"/><line x1="10" x2="21" y1="12" y2="12"/><line x1="10" x2="21" y1="18" y2="18"/><path d="M4 6h1v4"/><path d="M4 10h2"/><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"/>',
|
|
64
|
+
'quote': '<path d="M17 6H3"/><path d="M21 12H8"/><path d="M21 18H8"/><path d="M3 12v6"/>',
|
|
65
|
+
'message-square': '<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>',
|
|
66
|
+
'minus': '<path d="M5 12h14"/>',
|
|
67
|
+
'align-left': '<line x1="21" x2="3" y1="6" y2="6"/><line x1="15" x2="3" y1="12" y2="12"/><line x1="17" x2="3" y1="18" y2="18"/>',
|
|
68
|
+
'align-center': '<line x1="21" x2="3" y1="6" y2="6"/><line x1="17" x2="7" y1="12" y2="12"/><line x1="19" x2="5" y1="18" y2="18"/>',
|
|
69
|
+
'align-right': '<line x1="21" x2="3" y1="6" y2="6"/><line x1="21" x2="9" y1="12" y2="12"/><line x1="21" x2="7" y1="18" y2="18"/>',
|
|
70
|
+
'align-justify': '<line x1="3" x2="21" y1="6" y2="6"/><line x1="3" x2="21" y1="12" y2="12"/><line x1="3" x2="21" y1="18" y2="18"/>',
|
|
71
|
+
'eraser': '<path d="m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21"/><path d="M22 21H7"/><path d="m5 11 9 9"/>',
|
|
72
|
+
'undo': '<path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"/>',
|
|
73
|
+
'redo': '<path d="M21 7v6h-6"/><path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3l3 2.7"/>',
|
|
74
|
+
'link': '<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>',
|
|
75
|
+
'link-2': '<path d="M9 17H7A5 5 0 0 1 7 7h2"/><path d="M15 7h2a5 5 0 1 1 0 10h-2"/><line x1="8" x2="16" y1="12" y2="12"/>',
|
|
76
|
+
'external-link': '<path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h6"/>',
|
|
77
|
+
'triangle-alert': '<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/>',
|
|
78
|
+
'settings': '<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>',
|
|
79
|
+
'image': '<rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/>',
|
|
80
|
+
'newspaper': '<path d="M15 18h-5"/><path d="M18 14h-8"/><path d="M15 22H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v18a2 2 0 0 0 4 0v-9a2 2 0 0 0-2-2h-2"/><rect width="8" height="4" x="6" y="6" rx="1"/>',
|
|
81
|
+
'columns': '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="M15 3v18"/>',
|
|
82
|
+
'filter': '<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/>',
|
|
83
|
+
'search': '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
|
|
84
|
+
'sliders': '<line x1="21" x2="14" y1="4" y2="4"/><line x1="10" x2="3" y1="4" y2="4"/><line x1="21" x2="12" y1="12" y2="12"/><line x1="8" x2="3" y1="12" y2="12"/><line x1="21" x2="16" y1="20" y2="20"/><line x1="12" x2="3" y1="20" y2="20"/><line x1="14" x2="14" y1="2" y2="6"/><line x1="8" x2="8" y1="10" y2="14"/><line x1="16" x2="16" y1="18" y2="22"/>',
|
|
85
|
+
'eye': '<path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/>',
|
|
86
|
+
'eye-off': '<path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/><path d="m2 2 20 20"/>',
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A registry key, or arbitrary (developer-defined) inline SVG. The named-icon overloads keep editor
|
|
90
|
+
* autocomplete while allowing a collection to supply its own icon markup. */
|
|
91
|
+
export type IconSpec = IconName | (string & {})
|
|
92
|
+
|
|
93
|
+
export function isIconName(name: string): name is IconName {
|
|
94
|
+
return Object.hasOwn(icons, name)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Known-safe SVG elements/attributes. Names are lowercased for comparison; the original casing is
|
|
98
|
+
// preserved on output (`viewBox`, `linearGradient`, … are case-sensitive in SVG).
|
|
99
|
+
const ALLOWED_SVG_TAGS = new Set([
|
|
100
|
+
'svg', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'ellipse',
|
|
101
|
+
'g', 'defs', 'lineargradient', 'radialgradient', 'stop', 'clippath', 'mask',
|
|
102
|
+
'title', 'desc',
|
|
103
|
+
])
|
|
104
|
+
const ALLOWED_SVG_ATTRS = new Set([
|
|
105
|
+
'd', 'cx', 'cy', 'r', 'rx', 'ry', 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'fx', 'fy',
|
|
106
|
+
'width', 'height', 'points', 'transform', 'gradienttransform', 'viewbox', 'preserveaspectratio',
|
|
107
|
+
'fill', 'fill-rule', 'fill-opacity', 'stroke', 'stroke-width', 'stroke-linecap',
|
|
108
|
+
'stroke-linejoin', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-opacity', 'stroke-miterlimit',
|
|
109
|
+
'opacity', 'offset', 'stop-color', 'stop-opacity', 'gradientunits', 'spreadmethod',
|
|
110
|
+
'clip-path', 'clip-rule', 'id', 'class',
|
|
111
|
+
])
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Allowlist-sanitise developer-supplied icon SVG before it reaches `v-html` (Icon.vue). We keep ONLY
|
|
115
|
+
* known-safe SVG elements and attributes and drop everything else, rather than blocklisting bad patterns
|
|
116
|
+
* (a denylist is defeated by `/`-separated handlers, entity-encoded `javascript:`, etc.). So `<script>`,
|
|
117
|
+
* `<image>`, `<animate>`, `<a>`, `<use>`, `<foreignObject>`, every `on*` handler and every URL-bearing
|
|
118
|
+
* attribute are removed regardless of how they are written; no allowed element/attribute can load a URL
|
|
119
|
+
* or run script. Collection icons are developer-authored today, but this stays safe if collections become
|
|
120
|
+
* runtime-defined (the schema-engine roadmap), where `icon` would cross a user-controlled boundary.
|
|
121
|
+
*/
|
|
122
|
+
export function sanitizeIconSvg(svg: string): string {
|
|
123
|
+
return svg.replace(/<(\/?)([a-zA-Z][\w:-]*)((?:"[^"]*"|'[^']*'|[^>])*?)(\/?)>/g, (_m, close, tag, attrs, selfClose) => {
|
|
124
|
+
if (!ALLOWED_SVG_TAGS.has(tag.toLowerCase())) return ''
|
|
125
|
+
if (close) return `</${tag}>`
|
|
126
|
+
const kept: string[] = []
|
|
127
|
+
const attrRe = /([a-zA-Z_:][\w:.-]*)\s*=\s*("[^"]*"|'[^']*'|[^\s"'>]+)/g
|
|
128
|
+
let a: RegExpExecArray | null
|
|
129
|
+
while ((a = attrRe.exec(attrs))) {
|
|
130
|
+
if (ALLOWED_SVG_ATTRS.has(a[1].toLowerCase())) kept.push(`${a[1]}=${a[2]}`)
|
|
131
|
+
}
|
|
132
|
+
return `<${tag}${kept.length ? ' ' + kept.join(' ') : ''}${selfClose ? '/' : ''}>`
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Resolve an `IconSpec` to the inner SVG markup rendered inside the standard `<svg>` wrapper. A registry
|
|
138
|
+
* name yields its stored body; anything containing `<` is treated as raw inner-SVG markup (a full
|
|
139
|
+
* `<svg>…</svg>` is unwrapped to its children so it inherits the wrapper's size/`currentColor`), then
|
|
140
|
+
* sanitized. Unknown bare strings render nothing.
|
|
141
|
+
*/
|
|
142
|
+
export function resolveIconBody(name: string): string {
|
|
143
|
+
if (isIconName(name)) return icons[name]
|
|
144
|
+
if (!name.includes('<')) return ''
|
|
145
|
+
const inner = name.replace(/^\s*<svg[^>]*>([\s\S]*)<\/svg\s*>\s*$/i, '$1')
|
|
146
|
+
return sanitizeIconSvg(inner)
|
|
147
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { parseDate, parseDateTime, type DateValue } from '@internationalized/date'
|
|
2
|
+
|
|
3
|
+
export type IsoPrecision = 'date' | 'datetime'
|
|
4
|
+
|
|
5
|
+
/** ISO string → a `@internationalized/date` value for Reka; invalid/empty → undefined. */
|
|
6
|
+
export function isoToDate(iso: string | null | undefined, precision: IsoPrecision): DateValue | undefined {
|
|
7
|
+
if (!iso) return undefined
|
|
8
|
+
try {
|
|
9
|
+
return precision === 'date' ? parseDate(iso) : parseDateTime(iso)
|
|
10
|
+
} catch {
|
|
11
|
+
return undefined
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A `@internationalized/date` value → ISO string; null/undefined → null. */
|
|
16
|
+
export function dateToIso(value: DateValue | null | undefined): string | null {
|
|
17
|
+
return value ? value.toString() : null
|
|
18
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Localized } from '../../../core/server/utils/defineCollection'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve a (possibly localized) label for the active admin language. A plain string is returned as-is
|
|
5
|
+
* (language-agnostic); a `{ <lang>: string }` map resolves to the requested language, then `en`, then any
|
|
6
|
+
* present value. Returns `undefined` when there is nothing to show, so callers fall back (e.g. to the
|
|
7
|
+
* collection/block name). One source of label resolution for the nav, list, editor and block panes.
|
|
8
|
+
*/
|
|
9
|
+
export function resolveLocalized(value: Localized | undefined, lang: string): string | undefined {
|
|
10
|
+
if (value == null) return undefined
|
|
11
|
+
if (typeof value === 'string') return value
|
|
12
|
+
return value[lang] ?? value.en ?? Object.values(value)[0]
|
|
13
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { InjectionKey, Ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
/** Editor → field-widget context: lets a widget know the record's id (`'new'` until first save) and whether
|
|
4
|
+
* it has been saved. A field widget that manages out-of-band storage (e.g. the secure gallery's namespace)
|
|
5
|
+
* uses this to clean up after an abandoned, never-saved draft. Provided by `CollectionEditor`; mirrors the
|
|
6
|
+
* `blockEditKey` provide/inject precedent. Auto-imported (no path import needed in an extension). */
|
|
7
|
+
export interface RecordEditContext {
|
|
8
|
+
id: Ref<string>
|
|
9
|
+
saved: Ref<boolean>
|
|
10
|
+
/** The editor's reactive record values, so a widget can read SIBLING fields (e.g. the gallery `slug`,
|
|
11
|
+
* which the proofing overlay uses as the submission key). Reactive object — read `values.<field>`. */
|
|
12
|
+
values: Record<string, unknown>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const recordEditContextKey = Symbol('kestrel.recordEdit') as InjectionKey<RecordEditContext>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function reorder<T>(arr: readonly T[], from: number, to: number): T[] {
|
|
2
|
+
if (from === to || from < 0 || to < 0 || from >= arr.length || to >= arr.length) {
|
|
3
|
+
return [...arr]
|
|
4
|
+
}
|
|
5
|
+
const result = [...arr]
|
|
6
|
+
result.splice(to, 0, result.splice(from, 1)[0] as T)
|
|
7
|
+
return result
|
|
8
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Mark, Extension } from '@tiptap/vue-3' // re-exports @tiptap/core; keeps deps unchanged
|
|
2
|
+
|
|
3
|
+
// StarterKit has no span/TextStyle mark and no `class` attribute on block nodes, so parsing stored
|
|
4
|
+
// markup discards every consumer presentational class (e.g. the hero title's `text--color-white`).
|
|
5
|
+
// These two extensions teach the schema to keep `class` on span wrappers and on block nodes, matching
|
|
6
|
+
// what sanitize.ts already allows (`*: [class]`) so the class survives an edit/focus round-trip.
|
|
7
|
+
const classAttr = {
|
|
8
|
+
class: {
|
|
9
|
+
default: null as string | null,
|
|
10
|
+
parseHTML: (el: HTMLElement) => el.getAttribute('class'),
|
|
11
|
+
renderHTML: (a: Record<string, unknown>) => (a.class ? { class: a.class } : {}),
|
|
12
|
+
},
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Inline `<span class="…">` wrapper. Only spans that HAVE a class become this mark, so ordinary spans
|
|
16
|
+
// aren't captured.
|
|
17
|
+
export const RichtextSpanClass = Mark.create({
|
|
18
|
+
name: 'spanClass',
|
|
19
|
+
parseHTML: () => [{ tag: 'span[class]' }],
|
|
20
|
+
renderHTML: ({ HTMLAttributes }) => ['span', HTMLAttributes, 0],
|
|
21
|
+
addAttributes: () => classAttr,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// Block-level class (e.g. `<p class="text--section-label">`).
|
|
25
|
+
export const RichtextBlockClass = Extension.create({
|
|
26
|
+
name: 'blockClass',
|
|
27
|
+
addGlobalAttributes: () => [{
|
|
28
|
+
types: ['heading', 'paragraph', 'blockquote', 'bulletList', 'orderedList', 'listItem', 'codeBlock'],
|
|
29
|
+
attributes: classAttr,
|
|
30
|
+
}],
|
|
31
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { computed, useId, type ComputedRef } from 'vue'
|
|
2
|
+
|
|
3
|
+
interface FieldA11yProps {
|
|
4
|
+
hint?: string
|
|
5
|
+
error?: string | null
|
|
6
|
+
required?: boolean
|
|
7
|
+
id?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface FieldA11y {
|
|
11
|
+
fieldId: ComputedRef<string>
|
|
12
|
+
hintId: ComputedRef<string>
|
|
13
|
+
errId: ComputedRef<string>
|
|
14
|
+
describedby: ComputedRef<string | undefined>
|
|
15
|
+
ariaInvalid: ComputedRef<'true' | undefined>
|
|
16
|
+
ariaRequired: ComputedRef<'true' | undefined>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Shared accessibility wiring for labelled controls (UiField) and grouped controls (UiFieldset):
|
|
21
|
+
* a stable id, hint/error ids, the aria-describedby join, and aria-invalid. One source so the
|
|
22
|
+
* describedby ordering and aria contract cannot drift between the two.
|
|
23
|
+
*/
|
|
24
|
+
export function useFieldA11y(props: FieldA11yProps): FieldA11y {
|
|
25
|
+
const auto = useId()
|
|
26
|
+
const fieldId = computed(() => props.id ?? auto)
|
|
27
|
+
const hintId = computed(() => `${fieldId.value}-hint`)
|
|
28
|
+
const errId = computed(() => `${fieldId.value}-error`)
|
|
29
|
+
const describedby = computed(() => {
|
|
30
|
+
const ids: string[] = []
|
|
31
|
+
if (props.hint) ids.push(hintId.value)
|
|
32
|
+
if (props.error) ids.push(errId.value)
|
|
33
|
+
return ids.length ? ids.join(' ') : undefined
|
|
34
|
+
})
|
|
35
|
+
const ariaInvalid = computed(() => (props.error ? 'true' : undefined))
|
|
36
|
+
// Grouped/non-native controls (role=group, teleported widgets) get required conveyed via aria-required,
|
|
37
|
+
// since the visual asterisk is aria-hidden and a bare `required` attr on a non-input is ignored by AT.
|
|
38
|
+
const ariaRequired = computed(() => (props.required ? 'true' : undefined))
|
|
39
|
+
return { fieldId, hintId, errId, describedby, ariaInvalid, ariaRequired }
|
|
40
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// The UI design system (tokens / reset / base in app/assets/scss/main.scss) is intentionally NOT loaded
|
|
2
|
+
// app-globally — that would leak Kestrel's CSS reset + base styles onto a consumer's public site and fight
|
|
3
|
+
// their own CSS (e.g. Bootstrap). It is imported by the admin layout (layers/admin/app/layouts/admin.vue)
|
|
4
|
+
// instead, which scopes it to /admin: every admin route uses that layout, and /admin/** is ssr:false.
|
|
5
|
+
export default defineNuxtConfig({})
|
package/nuxt.config.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import kestrel from './kestrel.config'
|
|
2
|
+
import { layerKestrelOption } from './layers/core/server/utils/kestrel-config'
|
|
3
|
+
|
|
4
|
+
// Silence known-harmless, un-actionable THIRD-PARTY build noise (output unaffected — applied to both the
|
|
5
|
+
// Vite client and the Nitro server Rollup passes). Everything else still surfaces — crucially, any
|
|
6
|
+
// circular dependency that touches OUR `layers/` code is NOT dropped, so a real cycle we introduce stays
|
|
7
|
+
// visible. Anything in our own code is fixed, not silenced.
|
|
8
|
+
// - `#__PURE__`: @vueuse/core (pulled in via reka-ui) ships a `/* #__PURE__ */` annotation in a position
|
|
9
|
+
// Rollup can't interpret; it just drops the comment.
|
|
10
|
+
// - `module-preload-polyfill`: Nuxt's own preload-polyfill transform emits no sourcemap (cosmetic).
|
|
11
|
+
// - Circular dependencies entirely inside dependencies (nitropack's runtime internals: app.mjs ↔
|
|
12
|
+
// cache.mjs / utils.mjs, etc.) — upstream code we don't author and can't fix.
|
|
13
|
+
const SILENCED_BUILD_WARNINGS = ['#__PURE__', 'module-preload-polyfill']
|
|
14
|
+
// Typed inline — Rollup's `WarningHandlerWithDefault` isn't importable here (rollup is a transitive, not a
|
|
15
|
+
// direct, dep). `{ message?: string }` reads the bit we filter on; `defaultHandler` is structurally the
|
|
16
|
+
// rollup default handler (cast to call it with our wider warning shape).
|
|
17
|
+
const onwarn = (warning: { message?: string }, defaultHandler: (warning: never) => void) => {
|
|
18
|
+
const msg = warning.message ?? ''
|
|
19
|
+
if (SILENCED_BUILD_WARNINGS.some((s) => msg.includes(s))) return
|
|
20
|
+
// Third-party-internal cycles only (no `layers/` segment ⇒ none of our code is in the loop).
|
|
21
|
+
if (msg.includes('Circular dependency') && !msg.includes('layers/')) return
|
|
22
|
+
;(defaultHandler as (w: unknown) => void)(warning)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default defineNuxtConfig({
|
|
26
|
+
compatibilityDate: '2026-06-02',
|
|
27
|
+
future: { compatibilityVersion: 4 },
|
|
28
|
+
// `noUncheckedIndexedAccess` is a Nuxt tsconfig DEFAULT the project never opted into; the engine's code
|
|
29
|
+
// (runtime collection-derived tables → `Record<string, Column>` lookups, pervasive index access) was not
|
|
30
|
+
// written for it, so leaving it on would mean ~100 `!`-assertions that mostly HIDE undefined rather than
|
|
31
|
+
// catch it. Off → the `typecheck` gate is meaningful (real type errors surface) without that noise; it
|
|
32
|
+
// also keeps a consumer's own `nuxt typecheck` from drowning in errors against the shipped engine code.
|
|
33
|
+
// It must be set in BOTH places: `typescript.tsConfig` covers the app/shared/node tsconfigs, and
|
|
34
|
+
// `nitro.typescript.tsConfig` covers the separately-generated server (Nitro) tsconfig.
|
|
35
|
+
// Exclude tests from the typecheck (vitest covers them at runtime). The generated config's `include` uses
|
|
36
|
+
// parent-traversing paths (`../layers/…`), so the exclude globs must too — a bare `**/*.test.ts` won't match.
|
|
37
|
+
typescript: { tsConfig: { compilerOptions: { noUncheckedIndexedAccess: false }, exclude: ['../**/*.test.ts', '../../**/*.test.ts'] } },
|
|
38
|
+
// Explicit sub-layer composition so Kestrel works as an installable meta-layer (`extends: ['@michaelthielemann/kestrel']`):
|
|
39
|
+
// Nuxt does NOT transitively auto-scan an extended package's `layers/` dir, but it DOES follow its
|
|
40
|
+
// `extends` chain — so for a consumer this list is the ONLY thing that drives sub-layer order (core first).
|
|
41
|
+
// In-repo it is effectively inert: Nuxt also auto-scans the local `layers/` dir (resolved before the
|
|
42
|
+
// explicit `extends` and deduped by rootDir), and that auto-scan order is reverse-alphabetical — i.e.
|
|
43
|
+
// in-repo precedence is ui > public > media > fields > core > collections > auth > admin > access, NOT this order.
|
|
44
|
+
// The two contexts therefore differ; keep cross-layer basenames unique so the winner can't diverge.
|
|
45
|
+
extends: [
|
|
46
|
+
'./layers/core',
|
|
47
|
+
'./layers/fields',
|
|
48
|
+
'./layers/ui',
|
|
49
|
+
'./layers/media',
|
|
50
|
+
'./layers/auth',
|
|
51
|
+
'./layers/access',
|
|
52
|
+
'./layers/collections',
|
|
53
|
+
'./layers/admin',
|
|
54
|
+
'./layers/public',
|
|
55
|
+
],
|
|
56
|
+
// Prune `node_modules` (and the sibling `playground` app) from Nuxt's dev file-watcher. Nuxt's default
|
|
57
|
+
// `ignore` list does NOT include node_modules; when `@parcel/watcher` is unavailable Nuxt falls back to
|
|
58
|
+
// the granular chokidar watcher, whose recursively-spawned sub-watchers only apply `isIgnored` (not the
|
|
59
|
+
// `/node_modules/` guard the top watcher uses). Under chokidar 5 that walks the pnpm symlink farm in
|
|
60
|
+
// `node_modules/.pnpm` — including the `playground/node_modules/kestrel -> ..` cycle — unbounded, which
|
|
61
|
+
// pegs the event loop and OOMs `nuxt dev` a few minutes after boot. Ignoring node_modules here prunes it
|
|
62
|
+
// in every watcher mode (and benefits downstream consumers of this meta-layer, who'd hit the same bug).
|
|
63
|
+
// `/playground` is ANCHORED (leading slash) to this repo's own dev playground at the root — an unanchored
|
|
64
|
+
// `playground` is a gitignore-style any-depth match that would also exclude a downstream consumer's
|
|
65
|
+
// directory named `playground` from scanning. (`**/node_modules` independently prunes the OOM-causing farm.)
|
|
66
|
+
ignore: ['**/node_modules', '/playground'],
|
|
67
|
+
// `experimental.tasks` enables the `db:migrate` Nitro task — the explicit prod schema step (ADR-0002).
|
|
68
|
+
nitro: { externals: { external: ['better-sqlite3', 'sharp'] }, experimental: { tasks: true }, rollupConfig: { onwarn }, typescript: { tsConfig: { compilerOptions: { noUncheckedIndexedAccess: false } } } },
|
|
69
|
+
// Inline each prerendered page's critical CSS instead of a render-blocking <link> (the global CSS is
|
|
70
|
+
// small, so per-page inlining is cheap and removes a round-trip before first paint). SPA admin is
|
|
71
|
+
// ssr:false so it is unaffected. Explicit so the behaviour can't drift with a future Nuxt default.
|
|
72
|
+
features: { inlineStyles: true },
|
|
73
|
+
// NOTE: public pages ship the normal hydration runtime (`_nuxt`) — consumer projects build their own
|
|
74
|
+
// interactive Vue components, so a zero-JS (`noScripts`) default would break them. The runtime publisher
|
|
75
|
+
// depends on this too: `localFetch` only emits the `_nuxt` scripts + inline payload when noScripts is off.
|
|
76
|
+
// Single non-auth config source; the `kestrel` module (core layer) resolves it (config → KESTREL_* env
|
|
77
|
+
// → default) into runtimeConfig.media / runtimeConfig.public.locales + the /uploads static serving.
|
|
78
|
+
// `layerKestrelOption` contributes this file's values ONLY in-repo — when Kestrel is consumed as a
|
|
79
|
+
// dependency it yields `{}`, so Nuxt's array-concatenating layer merge can't leak the demo `locales`
|
|
80
|
+
// into a consumer (the consumer's own `kestrel: {}` is then the sole source).
|
|
81
|
+
kestrel: layerKestrelOption(kestrel, import.meta.url),
|
|
82
|
+
// Pre-bundle these so the dev server doesn't discover them at runtime and force a full reload.
|
|
83
|
+
vite: {
|
|
84
|
+
build: { rollupOptions: { onwarn } },
|
|
85
|
+
optimizeDeps: {
|
|
86
|
+
// Nested "kestrel > dep" form: Vite resolves each dep from the `kestrel` PACKAGE's directory
|
|
87
|
+
// rather than the (consumer) project root. Under pnpm these are kestrel's transitive deps and
|
|
88
|
+
// are NOT hoisted into a consumer's top-level node_modules, so a bare specifier is
|
|
89
|
+
// "Unresolvable" from the consumer root. In-repo `kestrel` is not a resolvable package name, so
|
|
90
|
+
// Vite's nestedResolveBasedir falls back to the repo root where these deps ARE hoisted — same
|
|
91
|
+
// result. The runtime `import('@tiptap/...')` still reuses the pre-bundle (tryOptimizedResolve
|
|
92
|
+
// matches the "> dep" id + src dir).
|
|
93
|
+
include: [
|
|
94
|
+
'kestrel > @internationalized/date',
|
|
95
|
+
'kestrel > @tiptap/extension-highlight',
|
|
96
|
+
'kestrel > @tiptap/extension-subscript',
|
|
97
|
+
'kestrel > @tiptap/extension-superscript',
|
|
98
|
+
'kestrel > @tiptap/extension-text-align',
|
|
99
|
+
'kestrel > @tiptap/starter-kit',
|
|
100
|
+
'kestrel > @tiptap/vue-3',
|
|
101
|
+
'kestrel > reka-ui',
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@michaelthielemann/kestrel",
|
|
3
|
+
"version": "1.2.1",
|
|
4
|
+
"description": "A slim, collection-driven Nuxt 4 CMS meta-layer with a runtime schema engine. Add `extends: ['@michaelthielemann/kestrel']`, define collections, and the database migrates itself.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "Michael Thielemann <283621694+MichaelThielemann@users.noreply.github.com>",
|
|
7
|
+
"homepage": "https://github.com/MichaelThielemann/kestrel#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/MichaelThielemann/kestrel.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": "https://github.com/MichaelThielemann/kestrel/issues",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"cms",
|
|
15
|
+
"nuxt",
|
|
16
|
+
"nuxt-layer",
|
|
17
|
+
"static-site-generator",
|
|
18
|
+
"drizzle",
|
|
19
|
+
"sqlite",
|
|
20
|
+
"headless-cms"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./nuxt.config.ts",
|
|
24
|
+
"packageManager": "pnpm@11.9.0",
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public",
|
|
27
|
+
"provenance": true
|
|
28
|
+
},
|
|
29
|
+
"//publish": "Consumed as a meta-layer: `extends: ['@michaelthielemann/kestrel']`. `main` points at the layer's nuxt.config so the BARE specifier resolves — without it c12 can't resolve the package, silently drops the whole layer (\"Cannot extend config from …\"), and every auto-import/component vanishes. Use `main`, NOT `exports` (exports would gate subpaths like `@michaelthielemann/kestrel/scripts/hash-password.mjs` + the deep config-type import). `files` ships the entry + sub-layers + operator scripts; the `!**/*.test.ts` globs strip tests. Releases run from the tag workflow via npm trusted publishing (OIDC), so no npm token exists anywhere.",
|
|
30
|
+
"files": [
|
|
31
|
+
"NOTICE",
|
|
32
|
+
"nuxt.config.ts",
|
|
33
|
+
"kestrel.config.ts",
|
|
34
|
+
"layers",
|
|
35
|
+
"scripts",
|
|
36
|
+
"!**/*.test.ts",
|
|
37
|
+
"!**/*.dom.test.ts",
|
|
38
|
+
"!**/*.nuxt.test.ts"
|
|
39
|
+
],
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=22.12.0"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"dev": "nuxt dev",
|
|
45
|
+
"build": "nuxt build",
|
|
46
|
+
"preview": "nuxt preview",
|
|
47
|
+
"generate": "nuxt generate",
|
|
48
|
+
"db:generate": "drizzle-kit generate",
|
|
49
|
+
"db:migrate": "drizzle-kit migrate",
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"test:watch": "vitest",
|
|
52
|
+
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
53
|
+
"test:nuxt": "vitest run --config vitest.nuxt.config.ts",
|
|
54
|
+
"typecheck": "node scripts/typecheck.mjs",
|
|
55
|
+
"hash-password": "node scripts/hash-password.mjs"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@babel/parser": "^7.28.0",
|
|
59
|
+
"@internationalized/date": "^3.12.2",
|
|
60
|
+
"@tiptap/extension-highlight": "^3.27.3",
|
|
61
|
+
"@tiptap/extension-subscript": "^3.27.3",
|
|
62
|
+
"@tiptap/extension-superscript": "^3.27.3",
|
|
63
|
+
"@tiptap/extension-text-align": "^3.27.3",
|
|
64
|
+
"@tiptap/pm": "^3.27.3",
|
|
65
|
+
"@tiptap/starter-kit": "^3.27.3",
|
|
66
|
+
"@tiptap/vue-3": "^3.27.3",
|
|
67
|
+
"aws4fetch": "^1.0.20",
|
|
68
|
+
"better-sqlite3": "^12.11.1",
|
|
69
|
+
"dompurify": "^3.4.12",
|
|
70
|
+
"drizzle-orm": "^0.45.2",
|
|
71
|
+
"drizzle-zod": "^0.8.3",
|
|
72
|
+
"file-type": "^22.0.1",
|
|
73
|
+
"jsdom": "^29.1.1",
|
|
74
|
+
"nanoid": "^5.1.16",
|
|
75
|
+
"nuxt": "^4.4.8",
|
|
76
|
+
"reka-ui": "^2.10.1",
|
|
77
|
+
"sanitize-html": "^2.17.5",
|
|
78
|
+
"sass": "^1.101.0",
|
|
79
|
+
"sharp": "^0.35.3",
|
|
80
|
+
"thumbhash": "^0.1.1",
|
|
81
|
+
"typescript": "^6.0.3",
|
|
82
|
+
"zod": "^4.4.3"
|
|
83
|
+
},
|
|
84
|
+
"//peers": "Framework singletons the layer sources import by bare specifier. Peers, not dependencies: a second copy breaks identity (two Vue instances; an h3 v1 `createError` handed to an h3 v2 error handler, which surfaces our 400/404/409 as bare 500s). All optional, because kestrel's own `nuxt` dependency already supplies them transitively — the entry pins identity where a copy exists rather than demanding one, and a required peer would ERESOLVE-fail npm installs on any Nuxt whose bundled range we did not anticipate. Each is also a devDependency so this repo resolves it without auto-install-peers writing a phantom root dependency into the lockfile.",
|
|
85
|
+
"peerDependencies": {
|
|
86
|
+
"@nuxt/kit": "^4.4.0",
|
|
87
|
+
"h3": "^1.15.0 || ^2.0.0",
|
|
88
|
+
"vue": "^3.5.0",
|
|
89
|
+
"vue-router": "^4.5.0 || ^5.0.0"
|
|
90
|
+
},
|
|
91
|
+
"peerDependenciesMeta": {
|
|
92
|
+
"@nuxt/kit": {
|
|
93
|
+
"optional": true
|
|
94
|
+
},
|
|
95
|
+
"h3": {
|
|
96
|
+
"optional": true
|
|
97
|
+
},
|
|
98
|
+
"vue": {
|
|
99
|
+
"optional": true
|
|
100
|
+
},
|
|
101
|
+
"vue-router": {
|
|
102
|
+
"optional": true
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"devDependencies": {
|
|
106
|
+
"@nuxt/kit": "^4.4.8",
|
|
107
|
+
"@nuxt/test-utils": "^3.23.0",
|
|
108
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
109
|
+
"@types/jsdom": "^28.0.3",
|
|
110
|
+
"@types/sanitize-html": "^2.16.1",
|
|
111
|
+
"@vitejs/plugin-vue": "^6.0.7",
|
|
112
|
+
"@vue/test-utils": "^2.4.11",
|
|
113
|
+
"drizzle-kit": "^0.31.10",
|
|
114
|
+
"h3": "^1.15.11",
|
|
115
|
+
"happy-dom": "^20.10.6",
|
|
116
|
+
"playwright-core": "^1.61.1",
|
|
117
|
+
"vitest": "^3.2.7",
|
|
118
|
+
"vue": "3.5.35",
|
|
119
|
+
"vue-router": "^5.1.0",
|
|
120
|
+
"vue-tsc": "^3.3.7"
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { scryptSync, randomBytes } from 'node:crypto'
|
|
2
|
+
import { createInterface } from 'node:readline'
|
|
3
|
+
|
|
4
|
+
const N = 2 ** 17
|
|
5
|
+
const r = 8
|
|
6
|
+
const p = 1
|
|
7
|
+
const KEYLEN = 64
|
|
8
|
+
const MAXMEM = 256 * 1024 * 1024
|
|
9
|
+
|
|
10
|
+
function make(password) {
|
|
11
|
+
const salt = randomBytes(16)
|
|
12
|
+
const hash = scryptSync(password, salt, KEYLEN, { N, r, p, maxmem: MAXMEM })
|
|
13
|
+
return `scrypt$${N}$${r}$${p}$${salt.toString('base64url')}$${hash.toString('base64url')}`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const arg = process.argv[2]
|
|
17
|
+
if (arg) {
|
|
18
|
+
process.stdout.write(make(arg) + '\n')
|
|
19
|
+
} else {
|
|
20
|
+
const rl = createInterface({ input: process.stdin, terminal: false })
|
|
21
|
+
process.stderr.write('Enter password, then press Enter:\n')
|
|
22
|
+
rl.on('line', (line) => {
|
|
23
|
+
process.stdout.write(make(line) + '\n')
|
|
24
|
+
rl.close()
|
|
25
|
+
})
|
|
26
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Typecheck gate. vitest/esbuild and `nuxt build` do NOT typecheck, so real type errors slip through
|
|
2
|
+
// every other gate — this is the one that catches them. It typechecks the PLAYGROUND, which composes
|
|
3
|
+
// the engine layers + both galleries extensions + a real consumer app/server, so this single run covers
|
|
4
|
+
// app, `.vue`, modules, config AND server code.
|
|
5
|
+
//
|
|
6
|
+
// Two passes, because Nuxt 4 splits the tsconfig by environment:
|
|
7
|
+
// 1. `tsc` over the Nitro server project (the app aggregator EXCLUDES server dirs).
|
|
8
|
+
// 2. `vue-tsc` (via `nuxt typecheck`) over the app/`.vue`/modules/config aggregator.
|
|
9
|
+
// Both exclude `*.test.ts` (vitest covers those at runtime) via the nuxt.config `typescript.tsConfig.exclude`
|
|
10
|
+
// and the derived server config below.
|
|
11
|
+
import { execFileSync } from 'node:child_process'
|
|
12
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
13
|
+
import { dirname, join } from 'node:path'
|
|
14
|
+
import { fileURLToPath } from 'node:url'
|
|
15
|
+
|
|
16
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
17
|
+
const playground = join(root, 'playground')
|
|
18
|
+
const bin = (name) => join(root, 'node_modules', '.bin', name)
|
|
19
|
+
const env = { ...process.env, CI: 'true', NUXT_TELEMETRY_DISABLED: '1' }
|
|
20
|
+
|
|
21
|
+
// 1. (Re)generate the Nuxt/Nitro tsconfigs for the playground.
|
|
22
|
+
execFileSync(bin('nuxt'), ['prepare'], { cwd: playground, stdio: 'inherit', env })
|
|
23
|
+
|
|
24
|
+
// 2. Server (Nitro) project — derive a test-excluding config alongside the generated one so its relative
|
|
25
|
+
// include/paths still resolve.
|
|
26
|
+
const generated = join(playground, '.nuxt', 'tsconfig.server.json')
|
|
27
|
+
const cfg = JSON.parse(readFileSync(generated, 'utf8'))
|
|
28
|
+
cfg.exclude = [...(cfg.exclude ?? []), '../../**/*.test.ts', '../**/*.test.ts']
|
|
29
|
+
const gateConfig = join(playground, '.nuxt', 'tsconfig.typecheck.json')
|
|
30
|
+
writeFileSync(gateConfig, JSON.stringify(cfg, null, 2))
|
|
31
|
+
// Both passes always run, and the gate fails at the END: aborting on the first one hides every error of
|
|
32
|
+
// the other, and the two cover disjoint file sets — a red server pass would report zero app errors.
|
|
33
|
+
const failed = []
|
|
34
|
+
function pass(label, cmd, args) {
|
|
35
|
+
console.log(`› typechecking ${label}…`)
|
|
36
|
+
try {
|
|
37
|
+
execFileSync(bin(cmd), args, { cwd: playground, stdio: 'inherit', env })
|
|
38
|
+
} catch {
|
|
39
|
+
failed.push(label)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
pass('server (Nitro) project', 'tsc', ['--noEmit', '-p', gateConfig])
|
|
44
|
+
// 3. App / `.vue` / modules / config — vue-tsc via `nuxt typecheck` (needs the committed root tsconfig.json).
|
|
45
|
+
pass('app + .vue (vue-tsc)', 'nuxt', ['typecheck'])
|
|
46
|
+
|
|
47
|
+
if (failed.length) {
|
|
48
|
+
console.error(`\n✗ typecheck failed: ${failed.join(', ')}`)
|
|
49
|
+
process.exit(1)
|
|
50
|
+
}
|
|
51
|
+
console.log('\n✓ typecheck passed (app + .vue + server; *.test.ts excluded)')
|