@michaelthielemann/kestrel 4.1.3 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -201
- package/dist/boot.d.ts +45 -0
- package/dist/boot.js +274 -0
- package/dist/cast.d.ts +3 -0
- package/dist/cast.js +6 -0
- package/dist/catalogue.d.ts +11 -0
- package/dist/catalogue.js +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +55 -0
- package/dist/context.d.ts +62 -0
- package/dist/context.js +61 -0
- package/dist/dataflow.d.ts +3 -0
- package/dist/dataflow.js +65 -0
- package/dist/defineConfig.d.ts +157 -0
- package/dist/defineConfig.js +41 -0
- package/dist/defineContract.d.ts +12 -0
- package/dist/defineContract.js +16 -0
- package/dist/defineModule.d.ts +75 -0
- package/dist/defineModule.js +9 -0
- package/dist/definePipeline.d.ts +7 -0
- package/dist/definePipeline.js +11 -0
- package/dist/errors.d.ts +26 -0
- package/dist/errors.js +48 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +14 -0
- package/dist/load.d.ts +7 -0
- package/dist/load.js +79 -0
- package/dist/logger.d.ts +18 -0
- package/dist/logger.js +36 -0
- package/dist/registry.d.ts +14 -0
- package/dist/registry.js +87 -0
- package/dist/result.d.ts +19 -0
- package/dist/result.js +19 -0
- package/dist/runner.d.ts +29 -0
- package/dist/runner.js +133 -0
- package/dist/schema.d.ts +7 -0
- package/dist/schema.js +142 -0
- package/dist/sort.d.ts +2 -0
- package/dist/sort.js +50 -0
- package/dist/testing/runPipeline.d.ts +9 -0
- package/dist/testing/runPipeline.js +22 -0
- package/dist/triggers/cron.d.ts +10 -0
- package/dist/triggers/cron.js +44 -0
- package/dist/triggers/cronMatch.d.ts +10 -0
- package/dist/triggers/cronMatch.js +68 -0
- package/dist/triggers/http.d.ts +58 -0
- package/dist/triggers/http.js +292 -0
- package/package.json +25 -151
- package/NOTICE +0 -12
- package/kestrel.config.ts +0 -28
- package/layers/access/nuxt.config.ts +0 -5
- package/layers/access/server/middleware/00.ip-allowlist.ts +0 -52
- package/layers/access/server/middleware/access-guard.ts +0 -32
- package/layers/access/server/types.d.ts +0 -7
- package/layers/admin/app/components/AdminAccount.vue +0 -212
- package/layers/admin/app/components/AdminNav.vue +0 -44
- package/layers/admin/app/components/BlockFields.vue +0 -67
- package/layers/admin/app/components/BlockPreview.vue +0 -413
- package/layers/admin/app/components/BlockTree.vue +0 -433
- package/layers/admin/app/components/BlocksBody.vue +0 -163
- package/layers/admin/app/components/CollectionDeleteDialog.vue +0 -38
- package/layers/admin/app/components/CollectionEditor.vue +0 -324
- package/layers/admin/app/components/CollectionList.vue +0 -429
- package/layers/admin/app/components/CollectionListBulkBar.vue +0 -62
- package/layers/admin/app/components/CollectionListColumnsPanel.vue +0 -37
- package/layers/admin/app/components/CollectionListFilterPanel.vue +0 -145
- package/layers/admin/app/components/CollectionListPager.vue +0 -70
- package/layers/admin/app/components/CollectionListTable.vue +0 -347
- package/layers/admin/app/components/EditorStatus.vue +0 -211
- package/layers/admin/app/components/EditorUnsupported.vue +0 -28
- package/layers/admin/app/components/FieldsBody.vue +0 -23
- package/layers/admin/app/components/LocaleBar.vue +0 -107
- package/layers/admin/app/components/PageFields.vue +0 -129
- package/layers/admin/app/components/PageFieldsPane.vue +0 -39
- package/layers/admin/app/components/SeoFields.vue +0 -210
- package/layers/admin/app/components/SingletonEditor.vue +0 -93
- package/layers/admin/app/composables/useAuth.ts +0 -68
- package/layers/admin/app/composables/useBlockTree.ts +0 -95
- package/layers/admin/app/composables/useBlocks.ts +0 -7
- package/layers/admin/app/composables/useCachedResource.ts +0 -35
- package/layers/admin/app/composables/useCollectionOps.ts +0 -102
- package/layers/admin/app/composables/useCollections.ts +0 -6
- package/layers/admin/app/composables/useContentLocales.ts +0 -23
- package/layers/admin/app/composables/useEditForm.ts +0 -402
- package/layers/admin/app/composables/useLinkResolver.ts +0 -40
- package/layers/admin/app/composables/useListBatchActions.ts +0 -78
- package/layers/admin/app/composables/useListColumns.ts +0 -53
- package/layers/admin/app/composables/useListFilterDraft.ts +0 -129
- package/layers/admin/app/composables/useListRows.ts +0 -75
- package/layers/admin/app/composables/useListSelection.ts +0 -28
- package/layers/admin/app/composables/useListUrlState.ts +0 -53
- package/layers/admin/app/composables/usePendingPublishPoll.ts +0 -15
- package/layers/admin/app/composables/usePreviewViewport.ts +0 -30
- package/layers/admin/app/composables/usePublishStatus.ts +0 -107
- package/layers/admin/app/composables/useRailCollapsed.ts +0 -14
- package/layers/admin/app/composables/useTheme.ts +0 -36
- package/layers/admin/app/composables/useToolbarPanel.ts +0 -38
- package/layers/admin/app/composables/useUnsavedGuard.ts +0 -22
- package/layers/admin/app/layouts/admin.vue +0 -266
- package/layers/admin/app/middleware/admin-auth.ts +0 -4
- package/layers/admin/app/pages/admin/[collection]/[id].vue +0 -203
- package/layers/admin/app/pages/admin/[collection]/index.vue +0 -62
- package/layers/admin/app/pages/admin/index.vue +0 -118
- package/layers/admin/app/pages/admin/login.vue +0 -68
- package/layers/admin/app/pages/admin/media.vue +0 -28
- package/layers/admin/app/pages/admin/references.vue +0 -146
- package/layers/admin/app/plugins/admin-style-guard.client.ts +0 -76
- package/layers/admin/app/plugins/reauth.client.ts +0 -20
- package/layers/admin/app/utils/admin-style-guard.ts +0 -20
- package/layers/admin/app/utils/block-tree.ts +0 -214
- package/layers/admin/app/utils/collection-ops.ts +0 -69
- package/layers/admin/app/utils/dead-refs.ts +0 -30
- package/layers/admin/app/utils/edit-form.ts +0 -267
- package/layers/admin/app/utils/editor-expose.ts +0 -36
- package/layers/admin/app/utils/editor-form-context.ts +0 -82
- package/layers/admin/app/utils/editor-registry.ts +0 -20
- package/layers/admin/app/utils/field-keys.ts +0 -11
- package/layers/admin/app/utils/list-cell.ts +0 -46
- package/layers/admin/app/utils/list-columns.ts +0 -142
- package/layers/admin/app/utils/list-query.ts +0 -96
- package/layers/admin/app/utils/nav-active.ts +0 -9
- package/layers/admin/app/utils/populate-blocks.ts +0 -288
- package/layers/admin/app/utils/preview-channel.ts +0 -62
- package/layers/admin/app/utils/preview-viewport.ts +0 -80
- package/layers/admin/app/utils/reauth.ts +0 -67
- package/layers/admin/app/utils/record-title.ts +0 -15
- package/layers/admin/app/utils/register-builtin-editors.ts +0 -9
- package/layers/admin/app/utils/safe-redirect.ts +0 -12
- package/layers/admin/nuxt.config.ts +0 -20
- package/layers/auth/nuxt.config.ts +0 -1
- package/layers/auth/server/plugins/01.register-auth-pipelines.ts +0 -7
- package/layers/collections/nuxt.config.ts +0 -1
- package/layers/collections/server/plugins/01.register.ts +0 -29
- package/layers/collections/server/plugins/02.register-relation-populate.ts +0 -18
- package/layers/core/app/composables/layouts.ts +0 -5
- package/layers/core/app/utils/field-factories.ts +0 -129
- package/layers/core/app/utils/layouts.ts +0 -38
- package/layers/core/modules/auto-discovery/extract-block.ts +0 -179
- package/layers/core/modules/auto-discovery/index.ts +0 -112
- package/layers/core/modules/auto-discovery/package-registry.ts +0 -19
- package/layers/core/modules/auto-discovery/scan.ts +0 -170
- package/layers/core/modules/auto-discovery/virtual.d.ts +0 -20
- package/layers/core/modules/component-namespace/index.ts +0 -44
- package/layers/core/modules/component-namespace/shared.ts +0 -24
- package/layers/core/modules/kestrel/app-shell.ts +0 -55
- package/layers/core/modules/kestrel/auto-imports.ts +0 -44
- package/layers/core/modules/kestrel/index.ts +0 -144
- package/layers/core/modules/plugin-order/index.ts +0 -69
- package/layers/core/modules/plugin-order/plugin-order.ts +0 -273
- package/layers/core/nuxt.config.ts +0 -10
- package/layers/core/server/api/[...path].ts +0 -98
- package/layers/core/server/plugins/00.config.ts +0 -12
- package/layers/core/server/plugins/00.migrate.ts +0 -12
- package/layers/core/server/plugins/01.register-introspection-pipeline.ts +0 -5
- package/layers/core/server/plugins/01.register-openapi-pipeline.ts +0 -5
- package/layers/core/server/plugins/01.register-tooling-pipelines.ts +0 -5
- package/layers/core/server/plugins/02.schema-sync.ts +0 -58
- package/layers/core/server/plugins/04.outbox-worker.ts +0 -31
- package/layers/core/server/plugins/05.reindex-refs.ts +0 -5
- package/layers/core/server/routes/__kestrel/dashboard.get.ts +0 -29
- package/layers/core/server/tasks/db/migrate-module.ts +0 -83
- package/layers/core/server/tasks/db/migrate-revisions.ts +0 -20
- package/layers/core/server/tasks/db/migrate.ts +0 -50
- package/layers/core/server/utils/server-config.ts +0 -81
- package/layers/fields/nuxt.config.ts +0 -1
- package/layers/fields/server/plugins/01.register-blocks-pipeline.ts +0 -7
- package/layers/fields/server/plugins/01.register-field-populate.ts +0 -11
- package/layers/media/app/components/KestrelImg.vue +0 -94
- package/layers/media/app/components/MediaConflictDialog.vue +0 -35
- package/layers/media/app/components/MediaDeleteDialog.vue +0 -52
- package/layers/media/app/components/MediaGrid.vue +0 -113
- package/layers/media/app/components/MediaImage.vue +0 -70
- package/layers/media/app/components/MediaLibrary.vue +0 -456
- package/layers/media/app/components/MediaNewFolderDialog.vue +0 -32
- package/layers/media/app/components/MediaPathBar.vue +0 -163
- package/layers/media/app/components/MediaPicker.vue +0 -19
- package/layers/media/app/components/MediaRenameDialog.vue +0 -32
- package/layers/media/app/components/MediaTable.vue +0 -75
- package/layers/media/app/components/MediaThumb.vue +0 -25
- package/layers/media/app/components/MediaToolbar.vue +0 -90
- package/layers/media/app/components/MediaUploadDialog.vue +0 -87
- package/layers/media/app/components/MediaViewer.vue +0 -137
- package/layers/media/app/components/field/Media.vue +0 -263
- package/layers/media/app/composables/useBlurUp.ts +0 -55
- package/layers/media/app/composables/useMediaClipboard.ts +0 -14
- package/layers/media/app/composables/useMediaContextMenu.ts +0 -56
- package/layers/media/app/composables/useMediaDnd.ts +0 -56
- package/layers/media/app/composables/useMediaLibrary.ts +0 -152
- package/layers/media/app/composables/useMediaOps.ts +0 -50
- package/layers/media/app/composables/useMediaRelocate.ts +0 -39
- package/layers/media/app/composables/useMediaResolver.ts +0 -50
- package/layers/media/app/composables/useMediaUpload.ts +0 -93
- package/layers/media/app/composables/useMediaVariant.ts +0 -34
- package/layers/media/app/plugins/field-media.client.ts +0 -6
- package/layers/media/app/utils/ai-disclosure.ts +0 -19
- package/layers/media/app/utils/dnd.ts +0 -69
- package/layers/media/app/utils/library.ts +0 -81
- package/layers/media/app/utils/ops.ts +0 -60
- package/layers/media/app/utils/picture-model.ts +0 -125
- package/layers/media/app/utils/thumbhash.ts +0 -19
- package/layers/media/app/utils/variant-name.ts +0 -20
- package/layers/media/nuxt.config.ts +0 -7
- package/layers/media/server/middleware/ondemand-variants.ts +0 -38
- package/layers/media/server/plugins/01.register-media-pipelines.ts +0 -7
- package/layers/media/server/plugins/02.register-media.ts +0 -22
- package/layers/media/server/plugins/04.variant-capture.ts +0 -27
- package/layers/media/server/plugins/05.media-cleanup.ts +0 -5
- package/layers/media/server/tasks/media/backfill.ts +0 -17
- package/layers/public/app/app.vue +0 -5
- package/layers/public/app/components/BlockRenderer.vue +0 -56
- package/layers/public/app/components/KestrelLink.vue +0 -26
- package/layers/public/app/components/KestrelPreviewBridge.vue +0 -133
- package/layers/public/app/composables/public-page.ts +0 -24
- package/layers/public/app/error.vue +0 -12
- package/layers/public/app/layouts/default.vue +0 -5
- package/layers/public/app/pages/[...slug].vue +0 -239
- package/layers/public/app/pages/__kestrel/preview.vue +0 -46
- package/layers/public/app/utils/block-edit-context.ts +0 -16
- package/layers/public/app/utils/json-ld.ts +0 -139
- package/layers/public/app/utils/link-href.ts +0 -46
- package/layers/public/app/utils/page-head.ts +0 -84
- package/layers/public/app/utils/page-layout.ts +0 -18
- package/layers/public/app/utils/preview-protocol.ts +0 -117
- package/layers/public/app/utils/public-route.ts +0 -46
- package/layers/public/app/utils/site-head.ts +0 -43
- package/layers/public/modules/deploy-output/index.ts +0 -80
- package/layers/public/modules/prerender-routes/discover.ts +0 -37
- package/layers/public/modules/prerender-routes/index.ts +0 -88
- package/layers/public/modules/prerender-routes/routes.ts +0 -16
- package/layers/public/modules/prune-media/index.ts +0 -48
- package/layers/public/modules/prune-media/prune-media.ts +0 -81
- package/layers/public/nuxt.config.ts +0 -14
- package/layers/public/server/delivery-live/serve.ts +0 -111
- package/layers/public/server/middleware/delivery-live-catchall.ts +0 -7
- package/layers/public/server/pipelines/index.ts +0 -23
- package/layers/public/server/plugins/00.ensure-snapshot-triggers.ts +0 -14
- package/layers/public/server/plugins/01.register-public-pipelines.ts +0 -7
- package/layers/public/server/plugins/02.register-links.ts +0 -16
- package/layers/public/server/plugins/03.redirects.ts +0 -55
- package/layers/public/server/plugins/05.plan-publish.ts +0 -5
- package/layers/public/server/plugins/zz.publish.ts +0 -127
- package/layers/public/server/routes/llms-full.txt.get.ts +0 -99
- package/layers/public/server/routes/llms.txt.get.ts +0 -66
- package/layers/public/server/routes/redirects.json.get.ts +0 -58
- package/layers/public/server/routes/robots.txt.get.ts +0 -11
- package/layers/public/server/routes/sitemap.xml.get.ts +0 -79
- package/layers/public/server/tasks/publish/run.ts +0 -32
- package/layers/public/server/utils/publish/render-live.ts +0 -26
- package/layers/ui/app/assets/scss/_base.scss +0 -16
- package/layers/ui/app/assets/scss/_datepicker.scss +0 -82
- package/layers/ui/app/assets/scss/_mixins.scss +0 -50
- package/layers/ui/app/assets/scss/_reset.scss +0 -65
- package/layers/ui/app/assets/scss/_tokens.scss +0 -183
- package/layers/ui/app/assets/scss/main.scss +0 -4
- package/layers/ui/app/components/field/Boolean.vue +0 -30
- package/layers/ui/app/components/field/Choice.vue +0 -73
- package/layers/ui/app/components/field/Datetime.vue +0 -98
- package/layers/ui/app/components/field/DeadRefNote.vue +0 -28
- package/layers/ui/app/components/field/Json.vue +0 -62
- package/layers/ui/app/components/field/Layout.vue +0 -138
- package/layers/ui/app/components/field/Link.vue +0 -112
- package/layers/ui/app/components/field/LinkInternalPicker.vue +0 -95
- package/layers/ui/app/components/field/Number.vue +0 -30
- package/layers/ui/app/components/field/Relation.vue +0 -49
- package/layers/ui/app/components/field/Renderer.vue +0 -30
- package/layers/ui/app/components/field/Repeater.vue +0 -460
- package/layers/ui/app/components/field/Richtext.vue +0 -26
- package/layers/ui/app/components/field/Slug.vue +0 -65
- package/layers/ui/app/components/field/Text.vue +0 -35
- package/layers/ui/app/components/field/Unsupported.vue +0 -34
- package/layers/ui/app/components/ui/Alert.vue +0 -50
- package/layers/ui/app/components/ui/Brand.vue +0 -22
- package/layers/ui/app/components/ui/Button.vue +0 -116
- package/layers/ui/app/components/ui/ButtonGroup.vue +0 -96
- package/layers/ui/app/components/ui/Checkbox.vue +0 -53
- package/layers/ui/app/components/ui/CheckboxGroup.vue +0 -51
- package/layers/ui/app/components/ui/Combobox.vue +0 -345
- package/layers/ui/app/components/ui/DatePicker.vue +0 -92
- package/layers/ui/app/components/ui/DateRangePicker.vue +0 -100
- package/layers/ui/app/components/ui/Dialog.vue +0 -107
- package/layers/ui/app/components/ui/EmptyState.vue +0 -46
- package/layers/ui/app/components/ui/Field.vue +0 -58
- package/layers/ui/app/components/ui/Fieldset.vue +0 -65
- package/layers/ui/app/components/ui/Icon.vue +0 -44
- package/layers/ui/app/components/ui/Menu.vue +0 -54
- package/layers/ui/app/components/ui/NumberInput.vue +0 -175
- package/layers/ui/app/components/ui/Richtext.vue +0 -141
- package/layers/ui/app/components/ui/RichtextToolbar.vue +0 -251
- package/layers/ui/app/components/ui/Select.vue +0 -52
- package/layers/ui/app/components/ui/TextInput.vue +0 -111
- package/layers/ui/app/components/ui/Textarea.vue +0 -39
- package/layers/ui/app/components/ui/TimeInput.vue +0 -18
- package/layers/ui/app/components/ui/Toasts.vue +0 -103
- package/layers/ui/app/components/ui/Tooltip.vue +0 -45
- package/layers/ui/app/composables/useAdminLang.ts +0 -8
- package/layers/ui/app/composables/useDragReorder.ts +0 -41
- package/layers/ui/app/composables/useEchoGuard.ts +0 -28
- package/layers/ui/app/composables/useLinkField.ts +0 -70
- package/layers/ui/app/composables/useRecordOptions.ts +0 -84
- package/layers/ui/app/composables/useRepeater.ts +0 -99
- package/layers/ui/app/composables/useT.ts +0 -32
- package/layers/ui/app/composables/useToast.ts +0 -46
- package/layers/ui/app/i18n/de.ts +0 -432
- package/layers/ui/app/i18n/en.ts +0 -434
- package/layers/ui/app/utils/boolean-field.ts +0 -10
- package/layers/ui/app/utils/field-component.ts +0 -22
- package/layers/ui/app/utils/field-empty.ts +0 -21
- package/layers/ui/app/utils/field-registry.ts +0 -36
- package/layers/ui/app/utils/humanize.ts +0 -14
- package/layers/ui/app/utils/icons.ts +0 -143
- package/layers/ui/app/utils/iso-date.ts +0 -18
- package/layers/ui/app/utils/localized.ts +0 -13
- package/layers/ui/app/utils/record-context.ts +0 -15
- package/layers/ui/app/utils/reorder.ts +0 -8
- package/layers/ui/app/utils/richtext-preserve-class.ts +0 -31
- package/layers/ui/app/utils/useFieldA11y.ts +0 -40
- package/layers/ui/nuxt.config.ts +0 -9
- package/nuxt.config.ts +0 -108
- package/scripts/consumer-template-ci.mjs +0 -160
- package/scripts/copy-create-payload.mjs +0 -49
- package/scripts/coverage-all.mjs +0 -163
- package/scripts/dashboard.mjs +0 -89
- package/scripts/hash-password.mjs +0 -29
- package/scripts/kestrel.mjs +0 -240
- package/scripts/lib/cli.mjs +0 -129
- package/scripts/lib/coverage-model.mjs +0 -51
- package/scripts/lib/password.mjs +0 -19
- package/scripts/lib/scaffold.mjs +0 -205
- package/scripts/port-graph.mjs +0 -350
- package/scripts/typecheck.mjs +0 -62
- package/templates/starter/README.md +0 -65
- package/templates/starter/_env.example +0 -17
- package/templates/starter/_gitignore +0 -26
- package/templates/starter/_package.json +0 -22
- package/templates/starter/app/app.vue +0 -7
- package/templates/starter/app/blocks/Prose.vue +0 -13
- package/templates/starter/app/layouts/default.vue +0 -6
- package/templates/starter/nuxt.config.ts +0 -23
- package/templates/starter/pnpm-workspace.yaml +0 -8
- package/templates/starter/tsconfig.json +0 -3
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { migrateRevisions, useDb, type MigrateRevisionsResult } from '@michaelthielemann/kestrel-core'
|
|
2
|
-
|
|
3
|
-
// Thin Nitro wrapper (defineTask is a runtime global) around the core, independently-unit-tested
|
|
4
|
-
// `migrateRevisions` (db/revision-migration.ts) — same sync/task split as db:migrate (schema/sync.ts +
|
|
5
|
-
// tasks/db/migrate.ts). Dev: GET/POST http://localhost:3000/_nitro/tasks/db:migrate-revisions ; prod: call
|
|
6
|
-
// runTask('db:migrate-revisions', { payload }) from inside the process — the built node-server has no task
|
|
7
|
-
// CLI/endpoint. Payload: {force:true} required, or the run refuses.
|
|
8
|
-
export default defineTask<MigrateRevisionsResult[]>({
|
|
9
|
-
meta: {
|
|
10
|
-
name: 'db:migrate-revisions',
|
|
11
|
-
description: 'Seed revision 1 for every existing row with no revision history yet. Requires {"force":true}.',
|
|
12
|
-
},
|
|
13
|
-
run({ payload }) {
|
|
14
|
-
const p = (payload ?? {}) as { force?: boolean }
|
|
15
|
-
const result = migrateRevisions(useDb(), { force: p.force === true })
|
|
16
|
-
const totalSeeded = result.reduce((n, r) => n + r.seeded, 0)
|
|
17
|
-
console.info(`[kestrel] db:migrate-revisions seeded ${totalSeeded} revision(s) across ${result.length} collection(s)`)
|
|
18
|
-
return { result }
|
|
19
|
-
},
|
|
20
|
-
})
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import collections from '#kestrel/collections'
|
|
2
|
-
import schemaTables from '#kestrel/schema-tables'
|
|
3
|
-
import type { AnySQLiteTable } from 'drizzle-orm/sqlite-core'
|
|
4
|
-
import { describeOp, desiredFromCollections, isDestructive, planOps, sqlite, syncSchema, useDb } from '@michaelthielemann/kestrel-core'
|
|
5
|
-
import type { BuiltCollection, CollectionDef, SyncDb } from '@michaelthielemann/kestrel-core'
|
|
6
|
-
import { resolveServerKestrel, serverRuntimeConfig } from '../../utils/server-config'
|
|
7
|
-
// Explicit schema-migrate step for production (ADR-0002). Dev auto-syncs additively at boot
|
|
8
|
-
// (02.schema-sync); prod boot never auto-DDLs, so an operator runs this task. Triggering (Nuxt 4.4 /
|
|
9
|
-
// Nitro 2.13 — there is NO `nuxi task run`):
|
|
10
|
-
// - dev: GET http://localhost:3000/_nitro/tasks/db:migrate (the dev-only task route)
|
|
11
|
-
// - prod: call runTask('db:migrate', { payload }) from inside the process (an authenticated route or a
|
|
12
|
-
// cron scheduledTask); the built node-server has no task CLI/endpoint.
|
|
13
|
-
// Payloads: {check:true}=dry-run (report pending DDL, change nothing); {force:true}=also apply REBUILDS
|
|
14
|
-
// (column drop/type change); {drop:["old_table"]}=drop a specific table (never bundled in force).
|
|
15
|
-
// Destructive ops are reported-but-withheld unless explicitly opted into; `force` never drops a table.
|
|
16
|
-
// The two run() branches return different result shapes (dry-run report vs applied/skipped); the explicit
|
|
17
|
-
// union is the task's result type so neither branch is forced to match the other.
|
|
18
|
-
type MigrateResult =
|
|
19
|
-
| { check: boolean; pending: string[]; destructive: string[] }
|
|
20
|
-
| { applied: string[]; skipped: string[] }
|
|
21
|
-
|
|
22
|
-
export default defineTask<MigrateResult>({
|
|
23
|
-
meta: { name: 'db:migrate', description: 'Apply schema changes; {check:true}=dry-run, {force:true}=rebuilds, {drop:[names]}=drop tables' },
|
|
24
|
-
run({ payload }) {
|
|
25
|
-
const p = (payload ?? {}) as { check?: boolean; force?: boolean; drop?: string[] }
|
|
26
|
-
const desired = desiredFromCollections(collections as (CollectionDef | BuiltCollection)[], {
|
|
27
|
-
// Same toggle source as the registry plugin, so runtime surface and schema agree (see 02.schema-sync).
|
|
28
|
-
toggles: (serverRuntimeConfig()?.kestrel?.collections ?? resolveServerKestrel().collections) as Record<string, boolean>,
|
|
29
|
-
extraTables: schemaTables as AnySQLiteTable[],
|
|
30
|
-
})
|
|
31
|
-
const client = (useDb() as unknown as { $client: SyncDb }).$client
|
|
32
|
-
// SQLite is the only backend today; this is the seam where a config-driven `resolveDialect()` would go.
|
|
33
|
-
const dialect = sqlite
|
|
34
|
-
|
|
35
|
-
if (p.check === true) {
|
|
36
|
-
const ops = planOps(client, desired, dialect)
|
|
37
|
-
const destructive = ops.filter(isDestructive).map(describeOp)
|
|
38
|
-
console.info(`[kestrel] db:migrate dry-run: ${ops.length} pending op(s), ${destructive.length} destructive`)
|
|
39
|
-
return { result: { check: true, pending: dialect.render(ops), destructive } }
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const { applied, skipped } = syncSchema(client, desired, {
|
|
43
|
-
allowDestructive: p.force === true,
|
|
44
|
-
dropTables: Array.isArray(p.drop) ? p.drop : undefined,
|
|
45
|
-
}, dialect)
|
|
46
|
-
if (skipped.length) console.warn(`[kestrel] db:migrate withheld ${skipped.length} destructive change(s) — opt in with {"force":true} (rebuilds) / {"drop":[…]} (drops):\n - ${skipped.map(describeOp).join('\n - ')}`)
|
|
47
|
-
console.info(`[kestrel] db:migrate applied ${applied.length} statement(s)`)
|
|
48
|
-
return { result: { applied, skipped: skipped.map(describeOp) } }
|
|
49
|
-
},
|
|
50
|
-
})
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import kestrelConfig from '../../../../kestrel.config'
|
|
2
|
-
import { resolveKestrel, type ResolvedKestrel } from '@michaelthielemann/kestrel-core'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* The Nitro runtimeConfig, or `undefined` for non-Nitro callers (node scripts, build) so they don't
|
|
6
|
-
* throw on `useRuntimeConfig()`. Server utils prefer the values the kestrel module put here (from the
|
|
7
|
-
* consumer's `kestrel: {}`), falling back to `resolveServerKestrel()` when it isn't populated.
|
|
8
|
-
*/
|
|
9
|
-
export function serverRuntimeConfig(): { kestrel?: Record<string, unknown>; media?: Record<string, unknown>; public?: Record<string, unknown> } | undefined {
|
|
10
|
-
return typeof useRuntimeConfig === 'function'
|
|
11
|
-
? (useRuntimeConfig() as { kestrel?: Record<string, unknown>; media?: Record<string, unknown>; public?: Record<string, unknown> })
|
|
12
|
-
: undefined
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Resolve Kestrel's own config file + env — the fallback when runtimeConfig isn't populated. */
|
|
16
|
-
export function resolveServerKestrel(): ReturnType<typeof resolveKestrel> {
|
|
17
|
-
return resolveKestrel(kestrelConfig, process.env, process.cwd())
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* The full `ResolvedKestrel` shape, merging every server util's own `runtimeConfig ?? resolveServerKestrel()`
|
|
22
|
-
* precedence into ONE call — the boot-time config-wiring plugin (`00.config.ts`) calls this once and
|
|
23
|
-
* pushes the result into the (eventually package-side) config provider, so package code never calls
|
|
24
|
-
* `useRuntimeConfig()` itself. Per-field precedence matches each field's existing reader exactly (the
|
|
25
|
-
* kestrel module writes a CLOSED set of fields into `runtimeConfig.kestrel`/`.public` — see
|
|
26
|
-
* `layers/core/modules/kestrel/index.ts`; anything it doesn't write there falls through to the
|
|
27
|
-
* `resolveServerKestrel()` base for every reader, unchanged from before this function existed).
|
|
28
|
-
*/
|
|
29
|
-
export function resolveServerKestrelConfig(): ResolvedKestrel {
|
|
30
|
-
const rc = serverRuntimeConfig()
|
|
31
|
-
const fallback = resolveServerKestrel()
|
|
32
|
-
const srv = (rc?.kestrel ?? {}) as Partial<Record<string, unknown>>
|
|
33
|
-
const pub = rc?.public as { locales?: string[]; primaryLocale?: string; prefixPrimary?: boolean } | undefined
|
|
34
|
-
// `runtimeConfig.media` is a TOP-LEVEL key (sibling to `.kestrel`/`.public`, not nested under `.kestrel`
|
|
35
|
-
// — see `layers/core/modules/kestrel/index.ts`'s `rc.media = {...}`), and it is the ONLY place the S3
|
|
36
|
-
// credentials + every Nitro `NUXT_MEDIA_*`/`NUXT_MEDIA_S3_*` env override (docs/guide/configuration.md) ever
|
|
37
|
-
// land — `resolveKestrel`'s own pure computation never sees them. Merged in wholesale (undefined ⇒ the
|
|
38
|
-
// module never ran ⇒ fall through to the pure fallback), mirroring `output`'s existing merge below.
|
|
39
|
-
const rcMedia = rc?.media as {
|
|
40
|
-
driver?: string
|
|
41
|
-
maxUploadBytes?: number
|
|
42
|
-
allowedMimes?: string
|
|
43
|
-
imagePolicy?: ResolvedKestrel['media']['imagePolicy']
|
|
44
|
-
local?: { dir?: string; baseUrl?: string }
|
|
45
|
-
s3?: { bucket?: string; region?: string; endpoint?: string; prefix?: string; publicBaseUrl?: string; accessKeyId?: string; secretAccessKey?: string; sessionToken?: string }
|
|
46
|
-
} | undefined
|
|
47
|
-
// The locale trio is populated together or not at all (see `locale.ts`'s original `resolved()`) — a
|
|
48
|
-
// partial `runtimeConfig.public` (e.g. `locales` present but `primaryLocale` absent) falls through to
|
|
49
|
-
// the fallback for all three rather than mixing sources mid-trio.
|
|
50
|
-
const locale = pub?.locales?.length && pub.primaryLocale
|
|
51
|
-
? { supportedLocales: pub.locales, primaryLocale: pub.primaryLocale, prefixPrimary: pub.prefixPrimary === true }
|
|
52
|
-
: { supportedLocales: fallback.supportedLocales, primaryLocale: fallback.primaryLocale, prefixPrimary: fallback.prefixPrimary }
|
|
53
|
-
return {
|
|
54
|
-
...fallback,
|
|
55
|
-
...locale,
|
|
56
|
-
dbPath: (srv.dbPath as string | undefined) ?? fallback.dbPath,
|
|
57
|
-
siteUrl: (srv.siteUrl as string | undefined) ?? fallback.siteUrl,
|
|
58
|
-
siteName: (srv.siteName as string | undefined) ?? fallback.siteName,
|
|
59
|
-
siteDescription: (srv.siteDescription as string | undefined) ?? fallback.siteDescription,
|
|
60
|
-
media: rcMedia ? {
|
|
61
|
-
dir: rcMedia.local?.dir ?? fallback.media.dir,
|
|
62
|
-
baseUrl: rcMedia.local?.baseUrl ?? fallback.media.baseUrl,
|
|
63
|
-
driver: (rcMedia.driver as 'local' | 's3' | undefined) ?? fallback.media.driver,
|
|
64
|
-
maxUploadBytes: rcMedia.maxUploadBytes ?? fallback.media.maxUploadBytes,
|
|
65
|
-
allowedMimes: rcMedia.allowedMimes ?? fallback.media.allowedMimes,
|
|
66
|
-
imagePolicy: rcMedia.imagePolicy ?? fallback.media.imagePolicy,
|
|
67
|
-
s3: { ...fallback.media.s3, ...rcMedia.s3 },
|
|
68
|
-
} : fallback.media,
|
|
69
|
-
collections: (srv.collections as ResolvedKestrel['collections'] | undefined) ?? fallback.collections,
|
|
70
|
-
aiDisclosure: (srv.aiDisclosure as ResolvedKestrel['aiDisclosure'] | undefined) ?? fallback.aiDisclosure,
|
|
71
|
-
seo: (srv.seo as ResolvedKestrel['seo'] | undefined) ?? fallback.seo,
|
|
72
|
-
delivery: (srv.delivery as ResolvedKestrel['delivery'] | undefined) ?? fallback.delivery,
|
|
73
|
-
deliveryExempt: (srv.deliveryExempt as string[] | undefined) ?? fallback.deliveryExempt,
|
|
74
|
-
output: (srv.output as ResolvedKestrel['output'] | undefined) ?? fallback.output,
|
|
75
|
-
// `revisions` is NOT written into `runtimeConfig.kestrel` by the module today (pre-existing —
|
|
76
|
-
// unrelated to this seam), so this reads as `fallback.revisions` in every real boot right now; kept
|
|
77
|
-
// symmetric with every other field rather than special-cased, so a future module fix (writing
|
|
78
|
-
// `srv.revisions`) is picked up here for free.
|
|
79
|
-
revisions: (srv.revisions as ResolvedKestrel['revisions'] | undefined) ?? fallback.revisions,
|
|
80
|
-
}
|
|
81
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export default defineNuxtConfig({})
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { registerPipeline } from '@michaelthielemann/kestrel-core'
|
|
2
|
-
import { buildBlocksPipelines } from '@michaelthielemann/kestrel-fields'
|
|
3
|
-
|
|
4
|
-
// Registration only — nothing resolves a pipeline here; the registry is read on the first request.
|
|
5
|
-
export default defineNitroPlugin(() => {
|
|
6
|
-
for (const def of buildBlocksPipelines()) registerPipeline(def)
|
|
7
|
-
})
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { registerPopulator } from '@michaelthielemann/kestrel-core'
|
|
2
|
-
import { buildFieldTreePopulator } from '@michaelthielemann/kestrel-fields'
|
|
3
|
-
|
|
4
|
-
// The single global row populator: walks a record's fields + block tree and dispatches each field to its
|
|
5
|
-
// per-type populator (media / link / richtext / relation), each registered by its owning layer. It recurses
|
|
6
|
-
// repeater entries, block props, and slots, so nested references resolve everywhere — not just at the top
|
|
7
|
-
// level. Field populators are looked up at read time, so this plugin's order vs the layer plugins that
|
|
8
|
-
// register them does not matter.
|
|
9
|
-
export default defineNitroPlugin(() => {
|
|
10
|
-
registerPopulator(buildFieldTreePopulator())
|
|
11
|
-
})
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { useBlurUp } from '../composables/useBlurUp'
|
|
3
|
-
import { useMediaVariant } from '../composables/useMediaVariant'
|
|
4
|
-
import { aiSourceTypeLabel } from '../utils/ai-disclosure'
|
|
5
|
-
import type { ResolvedMedia } from '@michaelthielemann/kestrel-media'
|
|
6
|
-
import type { VariantFit, VariantFormat } from '@michaelthielemann/kestrel-core'
|
|
7
|
-
|
|
8
|
-
// The engine's responsive-image component: it renders a <picture> from the media's generated derivatives
|
|
9
|
-
// AND its usage declares (to the generate-time scan) which sizes/formats are actually needed — "not in the
|
|
10
|
-
// code ⇒ not generated". One usage may declare MULTIPLE `widths` (proportional) and/or a fixed `crop`, plus
|
|
11
|
-
// named config `preset`s, crossed with `formats`. `priority` marks the LCP image (eager + high fetchpriority;
|
|
12
|
-
// no `sizes=auto`, which only applies to lazy images). `sizes="auto"` degrades gracefully (see resolveSizes).
|
|
13
|
-
const props = defineProps<{
|
|
14
|
-
media: ResolvedMedia | null | undefined
|
|
15
|
-
widths?: number[]
|
|
16
|
-
crop?: { width: number; height: number; fit?: VariantFit }
|
|
17
|
-
preset?: string | string[]
|
|
18
|
-
formats?: VariantFormat[]
|
|
19
|
-
sizes?: string
|
|
20
|
-
priority?: boolean
|
|
21
|
-
/** Opt into a structurally-present but UNSTYLED EU AI Act disclosure badge (`.kestrel-img__ai-badge`),
|
|
22
|
-
* rendered only when `media.aiDisclosure` is set. Off by default: Kestrel must never publish a claim
|
|
23
|
-
* the consumer did not ask for, in a place/language/style it cannot know. The alternative escape hatch
|
|
24
|
-
* is to read `media.aiDisclosure` and render your own element — see docs/guide/ai-disclosure.md. */
|
|
25
|
-
aiBadge?: boolean
|
|
26
|
-
}>()
|
|
27
|
-
|
|
28
|
-
const { model } = useMediaVariant(
|
|
29
|
-
() => props.media,
|
|
30
|
-
() => ({ widths: props.widths, crop: props.crop, preset: props.preset, formats: props.formats, sizes: props.sizes }),
|
|
31
|
-
{ priority: () => props.priority ?? false },
|
|
32
|
-
)
|
|
33
|
-
const { imgEl, animate, placeholderStyle, onLoad } = useBlurUp(() => props.media?.thumbhash)
|
|
34
|
-
</script>
|
|
35
|
-
|
|
36
|
-
<template>
|
|
37
|
-
<picture v-if="model">
|
|
38
|
-
<source v-for="s in model.sources" :key="s.type" :type="s.type" :srcset="s.srcset" :sizes="model.sizes">
|
|
39
|
-
<img
|
|
40
|
-
ref="imgEl"
|
|
41
|
-
:src="model.src"
|
|
42
|
-
:sizes="model.sizes"
|
|
43
|
-
:width="model.width ?? undefined"
|
|
44
|
-
:height="model.height ?? undefined"
|
|
45
|
-
:alt="model.alt"
|
|
46
|
-
:class="{ 'kestrel-img--in': animate }"
|
|
47
|
-
:style="placeholderStyle"
|
|
48
|
-
:loading="priority ? 'eager' : 'lazy'"
|
|
49
|
-
:fetchpriority="priority ? 'high' : undefined"
|
|
50
|
-
decoding="async"
|
|
51
|
-
@load="onLoad"
|
|
52
|
-
>
|
|
53
|
-
<span
|
|
54
|
-
v-if="aiBadge && media?.aiDisclosure"
|
|
55
|
-
class="kestrel-img__ai-badge"
|
|
56
|
-
:data-ai-source-type="media.aiDisclosure.sourceType"
|
|
57
|
-
>{{ media.aiDisclosure.note ?? aiSourceTypeLabel(media.aiDisclosure.sourceType) }}</span>
|
|
58
|
-
</picture>
|
|
59
|
-
</template>
|
|
60
|
-
|
|
61
|
-
<style scoped>
|
|
62
|
-
/* Sharpen-in: one-shot blur→crisp on a genuine async load (JS-driven, so SSR / no-JS / cached just show
|
|
63
|
-
the image). Same treatment as the demo MediaImage. */
|
|
64
|
-
@keyframes kestrel-img-sharpen {
|
|
65
|
-
from { filter: blur(8px); }
|
|
66
|
-
to { filter: blur(0); }
|
|
67
|
-
}
|
|
68
|
-
.kestrel-img--in {
|
|
69
|
-
animation: kestrel-img-sharpen 0.5s ease-out;
|
|
70
|
-
}
|
|
71
|
-
@media (prefers-reduced-motion: reduce) {
|
|
72
|
-
.kestrel-img--in {
|
|
73
|
-
animation: none;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
</style>
|
|
77
|
-
|
|
78
|
-
<!-- Deliberately UNSCOPED: a scoped rule carries the `[data-v-*]` attribute selector, which out-specifies
|
|
79
|
-
a consumer stylesheet targeting `.kestrel-img__ai-badge` and would make the badge un-restylable.
|
|
80
|
-
Layout only — no color, background, border, radius or font — so nothing is visible as a "badge" until
|
|
81
|
-
the consumer's own CSS says so. Scoped to pictures that actually contain one, so a consumer who never
|
|
82
|
-
sets `ai-badge` gets no rule at all. -->
|
|
83
|
-
<style>
|
|
84
|
-
picture:has(> .kestrel-img__ai-badge) {
|
|
85
|
-
position: relative;
|
|
86
|
-
display: inline-block;
|
|
87
|
-
}
|
|
88
|
-
.kestrel-img__ai-badge {
|
|
89
|
-
position: absolute;
|
|
90
|
-
inset-block-end: 0;
|
|
91
|
-
inset-inline-start: 0;
|
|
92
|
-
pointer-events: none;
|
|
93
|
-
}
|
|
94
|
-
</style>
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { computed } from 'vue'
|
|
3
|
-
import type { Conflict } from '../utils/ops'
|
|
4
|
-
|
|
5
|
-
const props = defineProps<{ open: boolean; conflicts: Conflict[]; type: 'move' | 'copy'; dest: string; busy?: boolean; error?: string | null }>()
|
|
6
|
-
const emit = defineEmits<{ resolve: ['skip' | 'overwrite' | 'rename']; 'update:open': [boolean] }>()
|
|
7
|
-
|
|
8
|
-
const { t } = useT()
|
|
9
|
-
const title = computed(() => props.type === 'move' ? t('media.conflictTitleMove') : t('media.conflictTitleCopy'))
|
|
10
|
-
</script>
|
|
11
|
-
|
|
12
|
-
<template>
|
|
13
|
-
<KestrelUiDialog :open="open" :title="title" @update:open="(v) => emit('update:open', v)">
|
|
14
|
-
<p>{{ t('media.conflictItems', { count: conflicts.length, dest: dest || t('media.rootFolder') }) }}</p>
|
|
15
|
-
<ul class="media-conflict__list">
|
|
16
|
-
<li v-for="c in conflicts" :key="c.targetPath">{{ c.targetPath }} — {{ c.type === 'folder-exists' ? t('media.conflictFolderExists') : t('media.conflictFileExists') }}</li>
|
|
17
|
-
</ul>
|
|
18
|
-
<KestrelUiAlert v-if="error" variant="error">{{ error }}</KestrelUiAlert>
|
|
19
|
-
<template #footer>
|
|
20
|
-
<KestrelUiButton :disabled="busy" @click="emit('update:open', false)">{{ t('common.cancel') }}</KestrelUiButton>
|
|
21
|
-
<KestrelUiButton :disabled="busy" @click="emit('resolve', 'skip')">{{ t('media.skip') }}</KestrelUiButton>
|
|
22
|
-
<KestrelUiButton :disabled="busy" @click="emit('resolve', 'overwrite')">{{ t('media.replace') }}</KestrelUiButton>
|
|
23
|
-
<KestrelUiButton variant="primary" :disabled="busy" @click="emit('resolve', 'rename')">{{ t('media.keepBoth') }}</KestrelUiButton>
|
|
24
|
-
</template>
|
|
25
|
-
</KestrelUiDialog>
|
|
26
|
-
</template>
|
|
27
|
-
|
|
28
|
-
<style lang="scss" scoped>
|
|
29
|
-
.media-conflict__list {
|
|
30
|
-
margin: var(--space-2) 0 0;
|
|
31
|
-
padding-left: var(--space-4);
|
|
32
|
-
font-size: var(--text-sm);
|
|
33
|
-
color: var(--color-text-muted);
|
|
34
|
-
}
|
|
35
|
-
</style>
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { computed } from 'vue'
|
|
3
|
-
import { humanizeSize } from '../utils/library'
|
|
4
|
-
import type { DeleteReport } from '../utils/ops'
|
|
5
|
-
|
|
6
|
-
const props = defineProps<{ open: boolean; report: DeleteReport | null; names: Record<number, string>; busy?: boolean; error?: string | null }>()
|
|
7
|
-
const emit = defineEmits<{ confirm: []; 'update:open': [boolean] }>()
|
|
8
|
-
|
|
9
|
-
const { t } = useT()
|
|
10
|
-
|
|
11
|
-
const usageLines = computed(() => {
|
|
12
|
-
const out: { file: string; refs: string }[] = []
|
|
13
|
-
for (const [id, refs] of Object.entries(props.report?.usages ?? {})) {
|
|
14
|
-
if (refs.length) out.push({ file: props.names[Number(id)] ?? `#${id}`, refs: refs.map((u) => `${u.collection} #${u.recordId} (${u.field})`).join(', ') })
|
|
15
|
-
}
|
|
16
|
-
return out
|
|
17
|
-
})
|
|
18
|
-
</script>
|
|
19
|
-
|
|
20
|
-
<template>
|
|
21
|
-
<KestrelUiDialog :open="open" :title="t('common.delete')" @update:open="(v) => emit('update:open', v)">
|
|
22
|
-
<p v-if="report">
|
|
23
|
-
{{ t('media.deleteSummary', {
|
|
24
|
-
files: report.summary.files,
|
|
25
|
-
filesUnit: report.summary.files === 1 ? t('media.file') : t('media.files'),
|
|
26
|
-
folders: report.summary.folders,
|
|
27
|
-
foldersUnit: report.summary.folders === 1 ? t('media.folder') : t('media.folders'),
|
|
28
|
-
size: humanizeSize(report.summary.totalBytes)
|
|
29
|
-
}) }}
|
|
30
|
-
</p>
|
|
31
|
-
<div v-if="usageLines.length" class="media-delete__usages">
|
|
32
|
-
<p>{{ t('media.usageWarning') }}</p>
|
|
33
|
-
<ul>
|
|
34
|
-
<li v-for="u in usageLines" :key="u.file">{{ u.file }} → {{ u.refs }}</li>
|
|
35
|
-
</ul>
|
|
36
|
-
</div>
|
|
37
|
-
<KestrelUiAlert v-if="error" variant="error">{{ error }}</KestrelUiAlert>
|
|
38
|
-
<template #footer>
|
|
39
|
-
<KestrelUiButton variant="ghost" :disabled="busy" @click="emit('update:open', false)">{{ t('common.cancel') }}</KestrelUiButton>
|
|
40
|
-
<KestrelUiButton variant="danger" :disabled="busy" @click="emit('confirm')">{{ t('common.delete') }}</KestrelUiButton>
|
|
41
|
-
</template>
|
|
42
|
-
</KestrelUiDialog>
|
|
43
|
-
</template>
|
|
44
|
-
|
|
45
|
-
<style lang="scss" scoped>
|
|
46
|
-
.media-delete__usages {
|
|
47
|
-
margin-top: var(--space-3);
|
|
48
|
-
color: var(--color-danger);
|
|
49
|
-
font-size: var(--text-sm);
|
|
50
|
-
ul { margin: var(--space-1) 0 0; padding-left: var(--space-4); }
|
|
51
|
-
}
|
|
52
|
-
</style>
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { itemKey, humanizeSize, type LibraryFile, type LibraryItem } from '../utils/library'
|
|
3
|
-
defineProps<{ items: LibraryItem[]; isSelected: (item: LibraryItem) => boolean; dropTargetPath?: string | null; parentPath?: string | null; upLabel?: string }>()
|
|
4
|
-
const emit = defineEmits<{ navigate: [string]; select: [LibraryItem, { toggle: boolean; range: boolean }]; open: [LibraryItem]; dragstart: [LibraryItem, DragEvent]; dragend: [] }>()
|
|
5
|
-
const mods = (e: MouseEvent) => ({ toggle: e.ctrlKey || e.metaKey, range: e.shiftKey })
|
|
6
|
-
// Plain click opens the folder; modifier-click selects it so folders can join the selection.
|
|
7
|
-
function onFolder(item: LibraryItem, path: string, e: MouseEvent) {
|
|
8
|
-
if (e.ctrlKey || e.metaKey || e.shiftKey) emit('select', item, mods(e))
|
|
9
|
-
else emit('navigate', path)
|
|
10
|
-
}
|
|
11
|
-
function onFile(item: LibraryItem, e: MouseEvent) { emit('select', item, mods(e)) }
|
|
12
|
-
const ext = (filename: string) => (filename.split('.').pop() ?? '').toUpperCase()
|
|
13
|
-
// Hover meta: dimensions (when known) · size — the same facts the table view shows in its columns.
|
|
14
|
-
function fileMeta(f: LibraryFile): string {
|
|
15
|
-
const parts: string[] = []
|
|
16
|
-
if (f.width && f.height) parts.push(`${f.width}×${f.height}`)
|
|
17
|
-
parts.push(humanizeSize(f.size))
|
|
18
|
-
return parts.join(' · ')
|
|
19
|
-
}
|
|
20
|
-
</script>
|
|
21
|
-
|
|
22
|
-
<template>
|
|
23
|
-
<ul class="media-grid">
|
|
24
|
-
<li v-if="parentPath != null" class="media-grid__cell">
|
|
25
|
-
<!-- Pure up-navigation; not selectable/draggable (kept out of `items`). -->
|
|
26
|
-
<button type="button" class="media-grid__tile media-grid__tile--folder" data-test="folder-up"
|
|
27
|
-
:aria-label="upLabel" @click="emit('navigate', parentPath)">
|
|
28
|
-
<span class="media-grid__thumb-wrap">
|
|
29
|
-
<span class="media-grid__thumb media-grid__thumb--folder" aria-hidden="true"><KestrelUiIcon name="folder" :size="40" /></span>
|
|
30
|
-
</span>
|
|
31
|
-
<span class="media-grid__name">..</span>
|
|
32
|
-
</button>
|
|
33
|
-
</li>
|
|
34
|
-
<li v-for="item in items" :key="itemKey(item)" class="media-grid__cell">
|
|
35
|
-
<button v-if="item.type === 'folder'" type="button" class="media-grid__tile media-grid__tile--folder"
|
|
36
|
-
:data-test="`folder-${item.folder.path}`" :aria-pressed="isSelected(item)"
|
|
37
|
-
:data-drop-folder="item.folder.path"
|
|
38
|
-
:class="{ 'is-selected': isSelected(item), 'is-drop-target': item.folder.path === dropTargetPath }"
|
|
39
|
-
draggable="true"
|
|
40
|
-
@click="onFolder(item, item.folder.path, $event)"
|
|
41
|
-
@keydown.space.prevent="emit('select', item, { toggle: true, range: false })"
|
|
42
|
-
@dragstart="(e) => emit('dragstart', item, e)"
|
|
43
|
-
@dragend="emit('dragend')">
|
|
44
|
-
<span class="media-grid__thumb-wrap">
|
|
45
|
-
<span class="media-grid__thumb media-grid__thumb--folder" aria-hidden="true"><KestrelUiIcon name="folder" :size="40" /></span>
|
|
46
|
-
<span v-if="item.folder.size != null" class="media-grid__meta" aria-hidden="true">{{ humanizeSize(item.folder.size) }}</span>
|
|
47
|
-
</span>
|
|
48
|
-
<span class="media-grid__name">{{ item.folder.name }}</span>
|
|
49
|
-
</button>
|
|
50
|
-
<button v-else type="button" class="media-grid__tile media-grid__tile--file"
|
|
51
|
-
:data-test="`file-${item.file.id}`" :aria-pressed="isSelected(item)"
|
|
52
|
-
:data-file-id="item.file.id"
|
|
53
|
-
:class="{ 'is-selected': isSelected(item) }"
|
|
54
|
-
draggable="true"
|
|
55
|
-
@click="onFile(item, $event)"
|
|
56
|
-
@dblclick="emit('open', item)"
|
|
57
|
-
@keydown.enter.prevent="emit('open', item)"
|
|
58
|
-
@dragstart="(e) => emit('dragstart', item, e)"
|
|
59
|
-
@dragend="emit('dragend')">
|
|
60
|
-
<span class="media-grid__thumb-wrap">
|
|
61
|
-
<!-- Show a thumbnail for any image WITH a source: media files carry `srcset` (responsive derivatives),
|
|
62
|
-
while a consumer feeding object-URLs (e.g. the secure gallery) has only `src`. Non-images → badge. -->
|
|
63
|
-
<KestrelMediaThumb v-if="item.file.mime?.startsWith('image/') && (item.file.srcset || item.file.src)" class="media-grid__thumb"
|
|
64
|
-
:src="item.file.src" :srcset="item.file.srcset" sizes="160px" :alt="item.file.alt ?? item.file.filename" :thumbhash="item.file.thumbhash" />
|
|
65
|
-
<span v-else class="media-grid__badge" aria-hidden="true">{{ ext(item.file.filename) }}</span>
|
|
66
|
-
<span class="media-grid__meta" aria-hidden="true">{{ fileMeta(item.file) }}</span>
|
|
67
|
-
<!-- Optional per-file overlay (e.g. proofing colour flags). Empty by default → nothing rendered. -->
|
|
68
|
-
<span class="media-grid__overlay"><slot name="file-overlay" :file="item.file" /></span>
|
|
69
|
-
</span>
|
|
70
|
-
<span class="media-grid__name">{{ item.file.filename }}</span>
|
|
71
|
-
</button>
|
|
72
|
-
</li>
|
|
73
|
-
</ul>
|
|
74
|
-
</template>
|
|
75
|
-
|
|
76
|
-
<style lang="scss" scoped>
|
|
77
|
-
.media-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr)); gap: var(--space-3); list-style: none; padding: 0; margin: 0; }
|
|
78
|
-
.media-grid__tile { display: flex; flex-direction: column; gap: var(--space-2); width: 100%; background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: var(--space-2); cursor: pointer; text-align: left; }
|
|
79
|
-
.media-grid__tile:hover { border-color: var(--color-border-strong); }
|
|
80
|
-
// Selection/drop/focus rings are INSET (border-color + inset shadow, or a negative outline-offset) so a
|
|
81
|
-
// scrolling ancestor never clips them — matches MediaTable. An outset outline (positive offset) on an
|
|
82
|
-
// edge tile is sliced off by the library's overflow.
|
|
83
|
-
.media-grid__tile.is-selected { border-color: var(--color-primary); box-shadow: inset 0 0 0 1px var(--color-primary); }
|
|
84
|
-
.media-grid__tile.is-drop-target { outline: 2px dashed var(--color-primary); outline-offset: -2px; background: var(--color-surface); }
|
|
85
|
-
.media-grid__tile:focus-visible { outline: 2px solid var(--color-focus); outline-offset: -2px; }
|
|
86
|
-
.media-grid__thumb-wrap { position: relative; width: 100%; }
|
|
87
|
-
.media-grid__overlay { position: absolute; top: var(--space-1); left: var(--space-1); display: flex; gap: 2px; pointer-events: none; }
|
|
88
|
-
.media-grid__thumb { aspect-ratio: 1; width: 100%; object-fit: cover; border-radius: var(--radius-sm); background: var(--color-bg); display: block; }
|
|
89
|
-
.media-grid__thumb--folder { display: grid; place-items: center; color: var(--color-text-subtle); }
|
|
90
|
-
.media-grid__badge { display: grid; place-items: center; aspect-ratio: 1; width: 100%; border-radius: var(--radius-sm); background: var(--color-bg); color: var(--color-text-muted); font-weight: var(--weight-bold); }
|
|
91
|
-
// Hover/focus reveal of the dimensions · size caption, overlaid on the lower edge of the thumb.
|
|
92
|
-
.media-grid__meta {
|
|
93
|
-
position: absolute;
|
|
94
|
-
inset-inline: 0;
|
|
95
|
-
bottom: 0;
|
|
96
|
-
padding: 2px var(--space-1);
|
|
97
|
-
font-size: var(--text-xs);
|
|
98
|
-
line-height: 1.35;
|
|
99
|
-
text-align: center;
|
|
100
|
-
white-space: nowrap;
|
|
101
|
-
overflow: hidden;
|
|
102
|
-
text-overflow: ellipsis;
|
|
103
|
-
color: var(--color-on-primary);
|
|
104
|
-
background: linear-gradient(transparent, var(--color-scrim));
|
|
105
|
-
border-radius: 0 0 var(--radius-sm) var(--radius-sm);
|
|
106
|
-
opacity: 0;
|
|
107
|
-
transition: opacity var(--motion-fast) var(--ease-standard);
|
|
108
|
-
pointer-events: none;
|
|
109
|
-
}
|
|
110
|
-
.media-grid__tile:hover .media-grid__meta,
|
|
111
|
-
.media-grid__tile:focus-visible .media-grid__meta { opacity: 1; }
|
|
112
|
-
.media-grid__name { font-size: var(--text-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
113
|
-
</style>
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { computed } from 'vue'
|
|
3
|
-
import { useBlurUp } from '../composables/useBlurUp'
|
|
4
|
-
|
|
5
|
-
interface ResolvedMedia {
|
|
6
|
-
alt: string | null
|
|
7
|
-
width: number | null
|
|
8
|
-
height: number | null
|
|
9
|
-
thumbhash: string | null
|
|
10
|
-
src: string
|
|
11
|
-
srcset: { url: string; width: number }[]
|
|
12
|
-
}
|
|
13
|
-
// `sizes` lets the consuming layout declare the rendered slot width (e.g.
|
|
14
|
-
// `(min-width: 768px) 50vw, 100vw`) so the browser picks an appropriately small
|
|
15
|
-
// `srcset` candidate instead of defaulting to 100vw and over-fetching the largest.
|
|
16
|
-
// `priority` marks the above-the-fold LCP image: load it eagerly with a high fetch priority instead of
|
|
17
|
-
// lazily, so the browser fetches it immediately rather than deferring the most important pixel. Opt-in
|
|
18
|
-
// per call site (the editor can't know which block is the LCP one); leave it off for everything else.
|
|
19
|
-
const props = defineProps<{ media: ResolvedMedia; sizes?: string; priority?: boolean }>()
|
|
20
|
-
// Computed, not a captured const: the live preview REUSES this instance (BlockRenderer keys blocks by id,
|
|
21
|
-
// consumer carousels key slides by index), so a media reorder/replace only patches the `media` prop — a
|
|
22
|
-
// one-shot const would keep the browser painting the OLD image via its stale `w`-descriptor candidate set.
|
|
23
|
-
const srcset = computed(() => props.media.srcset.map((s) => `${s.url} ${s.width}w`).join(', ') || undefined)
|
|
24
|
-
|
|
25
|
-
// Blur-up: a thumbhash placeholder shows until the raster paints, then a one-shot "sharpen-in" resolves
|
|
26
|
-
// the blur into the crisp image (see useBlurUp for the SSR/no-JS/cached-image degradation rules). The
|
|
27
|
-
// placeholder is painted as the <img>'s own background, so no wrapper element is introduced — the bare
|
|
28
|
-
// <img> keeps the public site's `img` box model (the reset's display:block / max-width:100%) intact.
|
|
29
|
-
const { imgEl, animate, placeholderStyle, onLoad } = useBlurUp(() => props.media.thumbhash)
|
|
30
|
-
</script>
|
|
31
|
-
|
|
32
|
-
<template>
|
|
33
|
-
<img
|
|
34
|
-
ref="imgEl"
|
|
35
|
-
:src="media.src"
|
|
36
|
-
:srcset="srcset"
|
|
37
|
-
:sizes="sizes"
|
|
38
|
-
:width="media.width ?? undefined"
|
|
39
|
-
:height="media.height ?? undefined"
|
|
40
|
-
:alt="media.alt ?? ''"
|
|
41
|
-
:class="{ 'media-image--in': animate }"
|
|
42
|
-
:style="placeholderStyle"
|
|
43
|
-
:loading="priority ? 'eager' : 'lazy'"
|
|
44
|
-
:fetchpriority="priority ? 'high' : undefined"
|
|
45
|
-
decoding="async"
|
|
46
|
-
@load="onLoad"
|
|
47
|
-
>
|
|
48
|
-
</template>
|
|
49
|
-
|
|
50
|
-
<style scoped>
|
|
51
|
-
/* Sharpen-in: when the real raster finishes loading it briefly starts soft and resolves to crisp, so the
|
|
52
|
-
swap from the thumbhash placeholder reads as a smooth focus pull rather than a hard snap. The class is
|
|
53
|
-
applied by JS only on a genuine async load, so SSR / no-JS / cached loads just show the image. */
|
|
54
|
-
@keyframes media-sharpen {
|
|
55
|
-
from {
|
|
56
|
-
filter: blur(8px);
|
|
57
|
-
}
|
|
58
|
-
to {
|
|
59
|
-
filter: blur(0);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
.media-image--in {
|
|
63
|
-
animation: media-sharpen 0.5s ease-out;
|
|
64
|
-
}
|
|
65
|
-
@media (prefers-reduced-motion: reduce) {
|
|
66
|
-
.media-image--in {
|
|
67
|
-
animation: none;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
</style>
|