@elytracms/studio 0.0.64 → 0.0.66
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/convex/assets.ts +11 -0
- package/convex/delivery.ts +5 -0
- package/convex/schema.ts +23 -0
- package/dist/components/account/self-account-view-source.js +9 -2
- package/dist/components/account/self-account-view-source.js.map +1 -1
- package/dist/components/app-views/default-modules.d.ts +28 -1
- package/dist/components/app-views/default-modules.js +47 -2
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/assets/asset-thumb.d.ts +4 -2
- package/dist/components/assets/asset-thumb.js +26 -4
- package/dist/components/assets/asset-thumb.js.map +1 -1
- package/dist/components/assets/assets-module.js +48 -2
- package/dist/components/assets/assets-module.js.map +1 -1
- package/dist/components/assets/assets-store.d.ts +11 -1
- package/dist/components/assets/assets-store.js +18 -1
- package/dist/components/assets/assets-store.js.map +1 -1
- package/dist/components/auth-context.js +32 -1
- package/dist/components/auth-context.js.map +1 -1
- package/dist/components/builder/blocks-model.d.ts +11 -0
- package/dist/components/builder/blocks-model.js +30 -10
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/canvas-chrome-css.js +90 -8
- package/dist/components/builder/canvas-chrome-css.js.map +1 -1
- package/dist/components/builder/canvas-dnd.js +4 -2
- package/dist/components/builder/canvas-dnd.js.map +1 -1
- package/dist/components/builder/canvas-inline-editing.d.ts +7 -0
- package/dist/components/builder/canvas-inline-editing.js +3 -1
- package/dist/components/builder/canvas-inline-editing.js.map +1 -1
- package/dist/components/builder/canvas-overlay-geometry.d.ts +72 -0
- package/dist/components/builder/canvas-overlay-geometry.js +80 -0
- package/dist/components/builder/canvas-overlay-geometry.js.map +1 -0
- package/dist/components/builder/canvas-rect-tracking.d.ts +124 -0
- package/dist/components/builder/canvas-rect-tracking.js +198 -0
- package/dist/components/builder/canvas-rect-tracking.js.map +1 -0
- package/dist/components/builder/canvas.js +61 -12
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/composition-field-editor.js +22 -3
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/delete-key.d.ts +38 -0
- package/dist/components/builder/delete-key.js +46 -0
- package/dist/components/builder/delete-key.js.map +1 -0
- package/dist/components/builder/drag-session.d.ts +8 -0
- package/dist/components/builder/drag-session.js +15 -0
- package/dist/components/builder/drag-session.js.map +1 -1
- package/dist/components/builder/insert-affordance-context.d.ts +19 -0
- package/dist/components/builder/insert-affordance-context.js +7 -0
- package/dist/components/builder/insert-affordance-context.js.map +1 -0
- package/dist/components/builder/insert-context.d.ts +40 -0
- package/dist/components/builder/insert-context.js +97 -5
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/node-title.d.ts +60 -0
- package/dist/components/builder/node-title.js +135 -0
- package/dist/components/builder/node-title.js.map +1 -0
- package/dist/components/builder/page-tree.js +25 -5
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/prop-selection.d.ts +52 -0
- package/dist/components/builder/prop-selection.js +38 -0
- package/dist/components/builder/prop-selection.js.map +1 -0
- package/dist/components/builder/prop-sync-context.d.ts +49 -0
- package/dist/components/builder/prop-sync-context.js +23 -0
- package/dist/components/builder/prop-sync-context.js.map +1 -0
- package/dist/components/builder/property-inspector.js +24 -1
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/builder/reuse-context.js +1 -1
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/selection-overlay.d.ts +79 -0
- package/dist/components/builder/selection-overlay.js +244 -0
- package/dist/components/builder/selection-overlay.js.map +1 -0
- package/dist/components/builder/spacing-handles.d.ts +57 -1
- package/dist/components/builder/spacing-handles.js +142 -26
- package/dist/components/builder/spacing-handles.js.map +1 -1
- package/dist/components/cms/asset-field.js +21 -5
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/asset-picker.js +11 -5
- package/dist/components/cms/asset-picker.js.map +1 -1
- package/dist/components/cms/bulk-actions.d.ts +22 -3
- package/dist/components/cms/bulk-actions.js +27 -3
- package/dist/components/cms/bulk-actions.js.map +1 -1
- package/dist/components/cms/cms-store.js +19 -0
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/detail-editor.js +18 -19
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/prop-fields.js +23 -16
- package/dist/components/cms/prop-fields.js.map +1 -1
- package/dist/components/cms/publish-clean.d.ts +21 -0
- package/dist/components/cms/publish-clean.js +30 -0
- package/dist/components/cms/publish-clean.js.map +1 -0
- package/dist/components/cms/rich-text/block-style.d.ts +48 -0
- package/dist/components/cms/rich-text/block-style.js +30 -0
- package/dist/components/cms/rich-text/block-style.js.map +1 -0
- package/dist/components/cms/rich-text/embed-picker.js +4 -3
- package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
- package/dist/components/cms/rich-text/extensions.d.ts +2 -0
- package/dist/components/cms/rich-text/extensions.js +4 -0
- package/dist/components/cms/rich-text/extensions.js.map +1 -1
- package/dist/components/cms/rich-text/font-size-control.d.ts +31 -0
- package/dist/components/cms/rich-text/font-size-control.js +40 -0
- package/dist/components/cms/rich-text/font-size-control.js.map +1 -0
- package/dist/components/cms/rich-text/font-size.d.ts +16 -0
- package/dist/components/cms/rich-text/font-size.js +40 -0
- package/dist/components/cms/rich-text/font-size.js.map +1 -0
- package/dist/components/cms/rich-text/toolbar-state.d.ts +3 -0
- package/dist/components/cms/rich-text/toolbar-state.js +3 -0
- package/dist/components/cms/rich-text/toolbar-state.js.map +1 -1
- package/dist/components/cms/rich-text/toolbar.d.ts +13 -0
- package/dist/components/cms/rich-text/toolbar.js +17 -5
- package/dist/components/cms/rich-text/toolbar.js.map +1 -1
- package/dist/components/cms/use-live-url.d.ts +24 -1
- package/dist/components/cms/use-live-url.js +26 -4
- package/dist/components/cms/use-live-url.js.map +1 -1
- package/dist/components/cms/use-publish-controller.js +8 -6
- package/dist/components/cms/use-publish-controller.js.map +1 -1
- package/dist/components/collection-view/cms-collection-view-source.js +15 -7
- package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
- package/dist/components/components/component-detail.js +3 -3
- package/dist/components/components/component-detail.js.map +1 -1
- package/dist/components/components/component-preview.js +1 -1
- package/dist/components/components/component-preview.js.map +1 -1
- package/dist/components/components/registry-browser.js +2 -2
- package/dist/components/components/registry-browser.js.map +1 -1
- package/dist/components/components/taxonomy.js +3 -2
- package/dist/components/components/taxonomy.js.map +1 -1
- package/dist/components/components/transfer-dialog.js +1 -1
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/lib/assets/backfill-thumbs.d.ts +67 -0
- package/dist/lib/assets/backfill-thumbs.js +120 -0
- package/dist/lib/assets/backfill-thumbs.js.map +1 -0
- package/dist/lib/assets/delete-asset.d.ts +2 -1
- package/dist/lib/assets/delete-asset.js +12 -0
- package/dist/lib/assets/delete-asset.js.map +1 -1
- package/dist/lib/assets/index.d.ts +2 -0
- package/dist/lib/assets/index.js +2 -0
- package/dist/lib/assets/index.js.map +1 -1
- package/dist/lib/assets/thumbnail.d.ts +145 -0
- package/dist/lib/assets/thumbnail.js +220 -0
- package/dist/lib/assets/thumbnail.js.map +1 -0
- package/dist/lib/assets/upload.d.ts +9 -0
- package/dist/lib/assets/upload.js +31 -0
- package/dist/lib/assets/upload.js.map +1 -1
- package/dist/lib/auth/auth-adapter.d.ts +22 -0
- package/dist/lib/auth/auth-adapter.js.map +1 -1
- package/dist/lib/auth/convex-auth-adapter.d.ts +42 -1
- package/dist/lib/auth/convex-auth-adapter.js +69 -0
- package/dist/lib/auth/convex-auth-adapter.js.map +1 -1
- package/dist/lib/auth/dev-auto-login-adapter.d.ts +2 -1
- package/dist/lib/auth/dev-auto-login-adapter.js +6 -0
- package/dist/lib/auth/dev-auto-login-adapter.js.map +1 -1
- package/dist/lib/auth/index.d.ts +1 -1
- package/dist/lib/auth/index.js.map +1 -1
- package/dist/lib/auth/local-auth-adapter.d.ts +2 -1
- package/dist/lib/auth/local-auth-adapter.js +7 -0
- package/dist/lib/auth/local-auth-adapter.js.map +1 -1
- package/dist/lib/auth-errors.d.ts +45 -3
- package/dist/lib/auth-errors.js +46 -5
- package/dist/lib/auth-errors.js.map +1 -1
- package/dist/lib/clipboard/paste-target.js +5 -2
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/dnd/drop-rules.d.ts +13 -0
- package/dist/lib/dnd/drop-rules.js +15 -0
- package/dist/lib/dnd/drop-rules.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +19 -0
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +16 -0
- package/dist/lib/i18n/messages.en.js +24 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.js +4 -0
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persistence/index.js +6 -3
- package/dist/lib/persistence/index.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +9 -5
- package/dist/lib/persona/labels.js +13 -8
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/references/references-store.js +7 -1
- package/dist/lib/references/references-store.js.map +1 -1
- package/dist/lib/session-expiry.d.ts +19 -6
- package/dist/lib/session-expiry.js +13 -1
- package/dist/lib/session-expiry.js.map +1 -1
- package/dist/lib/session-revalidation.d.ts +81 -0
- package/dist/lib/session-revalidation.js +94 -0
- package/dist/lib/session-revalidation.js.map +1 -0
- package/package.json +2 -2
- package/src/styles/studio.css +13 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transfer-dialog.js","sourceRoot":"","sources":["../../../src/components/components/transfer-dialog.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useState } from 'react'\nimport { FilePlus2, FileText, Loader2, Search } from 'lucide-react'\nimport type { ReactNode } from 'react'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport type { CmsDocument } from '@elytracms/core/cms-core'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router'\nimport {\n Button,\n Code,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Input,\n Row,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { getInstantiatedKit } from '@elytracms/studio/lib/starters/kit-session'\nimport { CmsSessionGate } from '../project-session'\nimport { CmsProvider, useCms } from '../cms/cms-store'\nimport { stageDocument } from '../cms/document-staging'\nimport { resolveCmsInitialCollections } from '../app-views/default-modules'\nimport {\n appendToBlockDocument,\n buildBlockDocument,\n canAppendTo,\n collectionsWithBlocks,\n isComponentAllowed,\n} from './transfer'\nimport type { CollectionBlocksTarget } from './transfer'\n\n/** Existing documents offered per target, beyond which the rest are hidden (filter to find them). */\nconst MAX_EXISTING_TARGETS = 8\n\n/**\n * EC-320 (+ 0.0.44 follow-up) — \"Use in document…\". Transfers the current\n * component combination (the live preview {@link ComponentNode}, edited props\n * and all) into a document's blocks field, then opens that document. Each\n * eligible collection+field target offers a NEW draft AND its existing\n * documents to append into.\n *\n * The dialog mounts a CMS session (so it reuses the store's create/save machinery,\n * collision-free ids and persistence) only while open. On confirm it builds the\n * draft or the append (pure {@link buildBlockDocument} / {@link appendToBlockDocument}),\n * saves it and AWAITS the persisted write landing (belt and braces alongside the\n * STAGE for the next CMS mount, see `document-staging` — both matter: staging\n * covers the very next mount regardless of mode, the await guards any later\n * reload), then navigates to the document — focused on its blocks field so it\n * opens on the canvas.\n */\nexport function TransferToDocumentDialog({\n projectId,\n manifest,\n node,\n open,\n onOpenChange,\n}: {\n projectId: string\n manifest: ComponentManifest\n node: ComponentNode\n open: boolean\n onOpenChange: (open: boolean) => void\n}) {\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>\n {translate('components.transfer.title', { name: manifest.title ?? manifest.id })}\n </DialogTitle>\n <DialogDescription>{translate('components.transfer.description')}</DialogDescription>\n </DialogHeader>\n {open ? (\n <CmsSessionGate>\n {(cms) => {\n const kit = getInstantiatedKit(projectId)\n // Bug fix: this used to reimplement `resolveCmsInitialCollections`'s\n // fallback chain inline with a final `[]` instead of `undefined` — `[]`\n // is \"present\" to `CmsProvider`'s `initialCollections ??\n // sampleFilterableCollections`, so it suppressed the fixture fallback a\n // fresh Convex-mode project (`cms === null`, no kit) needs. Delegating to\n // the shared, tested resolver (also used by the CMS/Assets tabs) keeps\n // this dialog's targets in sync with what those tabs show.\n const collections = resolveCmsInitialCollections(cms, kit)\n return (\n <CmsProvider\n projectId={projectId}\n initialCollections={collections}\n initialDocuments={cms?.documents ?? kit?.documents}\n initialPublishedKeys={cms?.publishedKeys}\n >\n <TransferPicker projectId={projectId} node={node} onClose={() => onOpenChange(false)} />\n </CmsProvider>\n )\n }}\n </CmsSessionGate>\n ) : null}\n </DialogContent>\n </Dialog>\n )\n}\n\n/** A pending transfer's in-flight target key: `<collectionId>/new` or `<collectionId>/<docId>`. */\nfunction newTargetKey(target: CollectionBlocksTarget): string {\n return `${target.collection.id}/new`\n}\nfunction existingTargetKey(target: CollectionBlocksTarget, doc: CmsDocument): string {\n return `${target.collection.id}/${doc.id}`\n}\n\n/**\n * Exported for the fresh-Convex-project regression test (bug fix,\n * transfer-dialog.test.tsx) — `TransferToDocumentDialog` is otherwise its only\n * caller.\n */\nexport function TransferPicker({\n projectId,\n node,\n onClose,\n}: {\n projectId: string\n node: ComponentNode\n onClose: () => void\n}) {\n const cms = useCms()\n const navigate = useNavigate()\n const [query, setQuery] = useState('')\n // The in-flight target's key while its save is awaited (disables every row\n // + shows a spinner on the clicked one) — `null` when idle.\n const [pendingKey, setPendingKey] = useState<string | null>(null)\n\n // Read the LIVE store schema (`cms.collections`) rather than re-deriving it\n // from the same `resolveCmsInitialCollections` result passed to `CmsProvider`\n // above — `cms.collections` is what the provider actually resolved to\n // (its OWN `initialCollections ?? sampleFilterableCollections` fallback,\n // EC-320), so the targets list can never drift from — or reintroduce the\n // `[]`-suppresses-the-fixture-fallback bug against — what the CMS/Assets\n // tabs show for this same session. Allow-gated (0.0.44 follow-up): a target\n // whose field's `allow` vocabulary rejects this component is never offered,\n // new-document or existing-document alike — the validator would flag it.\n const targets = useMemo(\n () => collectionsWithBlocks(cms.collections).filter((t) => isComponentAllowed(t.field, node.componentId)),\n [cms.collections, node.componentId],\n )\n\n /** Persist (awaited) + stage + navigate — shared by both transfer paths. */\n async function persistAndOpen(doc: CmsDocument, target: CollectionBlocksTarget) {\n // Optimistic session apply + queues the persisted upsert (EC-141's normal\n // fire-and-forget autosave path — unchanged for every OTHER caller).\n cms.saveDocument(doc)\n // Cross-mount handoff for the very next `CmsProvider` mount, in EVERY mode\n // (see document-staging.ts) — covers the instant after this write, before\n // any later hydration is guaranteed to observe it.\n stageDocument(projectId, doc)\n // Belt and braces: AWAIT the queued persistence command landing (the same\n // `flushPersistence` publish/unpublish already await) so a reload of the\n // destination editor — not just its very next mount — also finds the\n // document. `saveDocument` itself stays fire-and-forget for every other\n // caller; only this explicit await makes the transfer's save synchronous.\n await cms.flushPersistence()\n onClose()\n void navigate({\n to: STUDIO_PROJECT_ROUTE,\n params: { projectId, _splat: studioPath.cmsDocument(target.collection.id, doc.id) },\n search: { field: `values/${target.field.name}` },\n })\n }\n\n async function transferNew(target: CollectionBlocksTarget) {\n const key = newTargetKey(target)\n setPendingKey(key)\n try {\n const existingIds = new Set(cms.documentsOf(target.collection.id).map((d) => d.id))\n const doc = buildBlockDocument({\n collection: target.collection,\n blocksField: target.field,\n node,\n existingIds,\n seq: { current: 0 },\n })\n await persistAndOpen(doc, target)\n } finally {\n setPendingKey(null)\n }\n }\n\n async function transferInto(target: CollectionBlocksTarget, existing: CmsDocument) {\n const key = existingTargetKey(target, existing)\n setPendingKey(key)\n try {\n const doc = appendToBlockDocument({\n collection: target.collection,\n document: existing,\n blocksField: target.field,\n node,\n locale: cms.activeLocale,\n })\n await persistAndOpen(doc, target)\n } finally {\n setPendingKey(null)\n }\n }\n\n if (targets.length === 0) {\n return (\n <Text size=\"sm\" tone=\"muted\" data-testid=\"transfer-no-targets\">\n {translate('components.transfer.noTargets')}\n </Text>\n )\n }\n\n const trimmedQuery = query.trim().toLowerCase()\n const hasAnyExistingDocs = targets.some((t) => cms.documentsOf(t.collection.id).length > 0)\n\n return (\n <>\n {hasAnyExistingDocs ? (\n <div className=\"relative\">\n <Search className=\"pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground\" />\n <Input\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={translate('common.searchPlaceholder')}\n className=\"pl-8\"\n />\n </div>\n ) : null}\n <Stack gap=\"4\" className=\"max-h-[50vh] overflow-y-auto\">\n {targets.map((target) => {\n const existing = cms\n .documentsOf(target.collection.id)\n .filter((doc) => canAppendTo({ collection: target.collection, document: doc, blocksField: target.field, locale: cms.activeLocale }))\n .filter(\n (doc) =>\n trimmedQuery === '' ||\n cms.titleOf({ collection: target.collection.id, id: doc.id }).toLowerCase().includes(trimmedQuery),\n )\n .slice(0, MAX_EXISTING_TARGETS)\n\n return (\n <Stack key={target.collection.id} gap=\"2\">\n <Stack gap=\"px\">\n <Text size=\"sm\" weight=\"medium\" className=\"truncate\">\n {localizedLabel(target.collection.form?.label, target.collection.id)}\n </Text>\n <Row gap=\"1\" align=\"center\">\n <Text size=\"xs\" tone=\"muted\">\n {translate('components.transfer.intoField')}\n </Text>\n <Code tone=\"muted\">{target.field.name}</Code>\n </Row>\n </Stack>\n <Stack gap=\"1\">\n <TransferTargetRow\n testId=\"transfer-target\"\n icon={<FilePlus2 className=\"size-4\" />}\n label={translate('components.transfer.newDocument', {\n collection: localizedLabel(target.collection.form?.labelSingular, target.collection.id),\n })}\n pending={pendingKey === newTargetKey(target)}\n disabled={pendingKey !== null}\n onClick={() => void transferNew(target)}\n />\n {existing.length > 0 ? (\n <Text size=\"xs\" tone=\"muted\" className=\"px-1 pt-1\">\n {translate('components.transfer.orExisting')}\n </Text>\n ) : null}\n {existing.map((doc) => (\n <TransferTargetRow\n key={doc.id}\n testId=\"transfer-target-existing\"\n icon={<FileText className=\"size-4\" />}\n label={cms.titleOf({ collection: target.collection.id, id: doc.id })}\n pending={pendingKey === existingTargetKey(target, doc)}\n disabled={pendingKey !== null}\n onClick={() => void transferInto(target, doc)}\n />\n ))}\n </Stack>\n </Stack>\n )\n })}\n </Stack>\n <DialogFooter>\n <Button type=\"button\" variant=\"ghost\" onClick={onClose} disabled={pendingKey !== null}>\n {translate('common.cancel')}\n </Button>\n </DialogFooter>\n </>\n )\n}\n\n/** One selectable transfer target row — a new draft, or an existing document to append into. */\nfunction TransferTargetRow({\n testId,\n icon,\n label,\n pending,\n disabled,\n onClick,\n}: {\n testId: string\n icon: ReactNode\n label: string\n pending: boolean\n disabled: boolean\n onClick: () => void\n}) {\n return (\n <button\n type=\"button\"\n data-testid={testId}\n disabled={disabled}\n onClick={onClick}\n className={cn(\n 'flex items-center gap-3 rounded-md border border-border p-3 text-left transition-colors hover:border-primary/40 hover:bg-muted',\n disabled && 'cursor-not-allowed opacity-60',\n )}\n >\n <span className=\"grid size-8 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground\">\n {pending ? <Loader2 className=\"size-4 animate-spin\" /> : icon}\n </span>\n <Text size=\"sm\" weight=\"medium\" className=\"min-w-0 flex-1 truncate\">\n {label}\n </Text>\n </button>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"transfer-dialog.js","sourceRoot":"","sources":["../../../src/components/components/transfer-dialog.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useState } from 'react'\nimport { FilePlus2, FileText, Loader2, Search } from 'lucide-react'\nimport type { ReactNode } from 'react'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport type { CmsDocument } from '@elytracms/core/cms-core'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router'\nimport {\n Button,\n Code,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Input,\n Row,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { getInstantiatedKit } from '@elytracms/studio/lib/starters/kit-session'\nimport { CmsSessionGate } from '../project-session'\nimport { CmsProvider, useCms } from '../cms/cms-store'\nimport { stageDocument } from '../cms/document-staging'\nimport { resolveCmsInitialCollections } from '../app-views/default-modules'\nimport {\n appendToBlockDocument,\n buildBlockDocument,\n canAppendTo,\n collectionsWithBlocks,\n isComponentAllowed,\n} from './transfer'\nimport type { CollectionBlocksTarget } from './transfer'\n\n/** Existing documents offered per target, beyond which the rest are hidden (filter to find them). */\nconst MAX_EXISTING_TARGETS = 8\n\n/**\n * EC-320 (+ 0.0.44 follow-up) — \"Use in document…\". Transfers the current\n * component combination (the live preview {@link ComponentNode}, edited props\n * and all) into a document's blocks field, then opens that document. Each\n * eligible collection+field target offers a NEW draft AND its existing\n * documents to append into.\n *\n * The dialog mounts a CMS session (so it reuses the store's create/save machinery,\n * collision-free ids and persistence) only while open. On confirm it builds the\n * draft or the append (pure {@link buildBlockDocument} / {@link appendToBlockDocument}),\n * saves it and AWAITS the persisted write landing (belt and braces alongside the\n * STAGE for the next CMS mount, see `document-staging` — both matter: staging\n * covers the very next mount regardless of mode, the await guards any later\n * reload), then navigates to the document — focused on its blocks field so it\n * opens on the canvas.\n */\nexport function TransferToDocumentDialog({\n projectId,\n manifest,\n node,\n open,\n onOpenChange,\n}: {\n projectId: string\n manifest: ComponentManifest\n node: ComponentNode\n open: boolean\n onOpenChange: (open: boolean) => void\n}) {\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>\n {translate('components.transfer.title', { name: localizedLabel(manifest.title, manifest.id) })}\n </DialogTitle>\n <DialogDescription>{translate('components.transfer.description')}</DialogDescription>\n </DialogHeader>\n {open ? (\n <CmsSessionGate>\n {(cms) => {\n const kit = getInstantiatedKit(projectId)\n // Bug fix: this used to reimplement `resolveCmsInitialCollections`'s\n // fallback chain inline with a final `[]` instead of `undefined` — `[]`\n // is \"present\" to `CmsProvider`'s `initialCollections ??\n // sampleFilterableCollections`, so it suppressed the fixture fallback a\n // fresh Convex-mode project (`cms === null`, no kit) needs. Delegating to\n // the shared, tested resolver (also used by the CMS/Assets tabs) keeps\n // this dialog's targets in sync with what those tabs show.\n const collections = resolveCmsInitialCollections(cms, kit)\n return (\n <CmsProvider\n projectId={projectId}\n initialCollections={collections}\n initialDocuments={cms?.documents ?? kit?.documents}\n initialPublishedKeys={cms?.publishedKeys}\n >\n <TransferPicker projectId={projectId} node={node} onClose={() => onOpenChange(false)} />\n </CmsProvider>\n )\n }}\n </CmsSessionGate>\n ) : null}\n </DialogContent>\n </Dialog>\n )\n}\n\n/** A pending transfer's in-flight target key: `<collectionId>/new` or `<collectionId>/<docId>`. */\nfunction newTargetKey(target: CollectionBlocksTarget): string {\n return `${target.collection.id}/new`\n}\nfunction existingTargetKey(target: CollectionBlocksTarget, doc: CmsDocument): string {\n return `${target.collection.id}/${doc.id}`\n}\n\n/**\n * Exported for the fresh-Convex-project regression test (bug fix,\n * transfer-dialog.test.tsx) — `TransferToDocumentDialog` is otherwise its only\n * caller.\n */\nexport function TransferPicker({\n projectId,\n node,\n onClose,\n}: {\n projectId: string\n node: ComponentNode\n onClose: () => void\n}) {\n const cms = useCms()\n const navigate = useNavigate()\n const [query, setQuery] = useState('')\n // The in-flight target's key while its save is awaited (disables every row\n // + shows a spinner on the clicked one) — `null` when idle.\n const [pendingKey, setPendingKey] = useState<string | null>(null)\n\n // Read the LIVE store schema (`cms.collections`) rather than re-deriving it\n // from the same `resolveCmsInitialCollections` result passed to `CmsProvider`\n // above — `cms.collections` is what the provider actually resolved to\n // (its OWN `initialCollections ?? sampleFilterableCollections` fallback,\n // EC-320), so the targets list can never drift from — or reintroduce the\n // `[]`-suppresses-the-fixture-fallback bug against — what the CMS/Assets\n // tabs show for this same session. Allow-gated (0.0.44 follow-up): a target\n // whose field's `allow` vocabulary rejects this component is never offered,\n // new-document or existing-document alike — the validator would flag it.\n const targets = useMemo(\n () => collectionsWithBlocks(cms.collections).filter((t) => isComponentAllowed(t.field, node.componentId)),\n [cms.collections, node.componentId],\n )\n\n /** Persist (awaited) + stage + navigate — shared by both transfer paths. */\n async function persistAndOpen(doc: CmsDocument, target: CollectionBlocksTarget) {\n // Optimistic session apply + queues the persisted upsert (EC-141's normal\n // fire-and-forget autosave path — unchanged for every OTHER caller).\n cms.saveDocument(doc)\n // Cross-mount handoff for the very next `CmsProvider` mount, in EVERY mode\n // (see document-staging.ts) — covers the instant after this write, before\n // any later hydration is guaranteed to observe it.\n stageDocument(projectId, doc)\n // Belt and braces: AWAIT the queued persistence command landing (the same\n // `flushPersistence` publish/unpublish already await) so a reload of the\n // destination editor — not just its very next mount — also finds the\n // document. `saveDocument` itself stays fire-and-forget for every other\n // caller; only this explicit await makes the transfer's save synchronous.\n await cms.flushPersistence()\n onClose()\n void navigate({\n to: STUDIO_PROJECT_ROUTE,\n params: { projectId, _splat: studioPath.cmsDocument(target.collection.id, doc.id) },\n search: { field: `values/${target.field.name}` },\n })\n }\n\n async function transferNew(target: CollectionBlocksTarget) {\n const key = newTargetKey(target)\n setPendingKey(key)\n try {\n const existingIds = new Set(cms.documentsOf(target.collection.id).map((d) => d.id))\n const doc = buildBlockDocument({\n collection: target.collection,\n blocksField: target.field,\n node,\n existingIds,\n seq: { current: 0 },\n })\n await persistAndOpen(doc, target)\n } finally {\n setPendingKey(null)\n }\n }\n\n async function transferInto(target: CollectionBlocksTarget, existing: CmsDocument) {\n const key = existingTargetKey(target, existing)\n setPendingKey(key)\n try {\n const doc = appendToBlockDocument({\n collection: target.collection,\n document: existing,\n blocksField: target.field,\n node,\n locale: cms.activeLocale,\n })\n await persistAndOpen(doc, target)\n } finally {\n setPendingKey(null)\n }\n }\n\n if (targets.length === 0) {\n return (\n <Text size=\"sm\" tone=\"muted\" data-testid=\"transfer-no-targets\">\n {translate('components.transfer.noTargets')}\n </Text>\n )\n }\n\n const trimmedQuery = query.trim().toLowerCase()\n const hasAnyExistingDocs = targets.some((t) => cms.documentsOf(t.collection.id).length > 0)\n\n return (\n <>\n {hasAnyExistingDocs ? (\n <div className=\"relative\">\n <Search className=\"pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground\" />\n <Input\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={translate('common.searchPlaceholder')}\n className=\"pl-8\"\n />\n </div>\n ) : null}\n <Stack gap=\"4\" className=\"max-h-[50vh] overflow-y-auto\">\n {targets.map((target) => {\n const existing = cms\n .documentsOf(target.collection.id)\n .filter((doc) => canAppendTo({ collection: target.collection, document: doc, blocksField: target.field, locale: cms.activeLocale }))\n .filter(\n (doc) =>\n trimmedQuery === '' ||\n cms.titleOf({ collection: target.collection.id, id: doc.id }).toLowerCase().includes(trimmedQuery),\n )\n .slice(0, MAX_EXISTING_TARGETS)\n\n return (\n <Stack key={target.collection.id} gap=\"2\">\n <Stack gap=\"px\">\n <Text size=\"sm\" weight=\"medium\" className=\"truncate\">\n {localizedLabel(target.collection.form?.label, target.collection.id)}\n </Text>\n <Row gap=\"1\" align=\"center\">\n <Text size=\"xs\" tone=\"muted\">\n {translate('components.transfer.intoField')}\n </Text>\n <Code tone=\"muted\">{target.field.name}</Code>\n </Row>\n </Stack>\n <Stack gap=\"1\">\n <TransferTargetRow\n testId=\"transfer-target\"\n icon={<FilePlus2 className=\"size-4\" />}\n label={translate('components.transfer.newDocument', {\n collection: localizedLabel(target.collection.form?.labelSingular, target.collection.id),\n })}\n pending={pendingKey === newTargetKey(target)}\n disabled={pendingKey !== null}\n onClick={() => void transferNew(target)}\n />\n {existing.length > 0 ? (\n <Text size=\"xs\" tone=\"muted\" className=\"px-1 pt-1\">\n {translate('components.transfer.orExisting')}\n </Text>\n ) : null}\n {existing.map((doc) => (\n <TransferTargetRow\n key={doc.id}\n testId=\"transfer-target-existing\"\n icon={<FileText className=\"size-4\" />}\n label={cms.titleOf({ collection: target.collection.id, id: doc.id })}\n pending={pendingKey === existingTargetKey(target, doc)}\n disabled={pendingKey !== null}\n onClick={() => void transferInto(target, doc)}\n />\n ))}\n </Stack>\n </Stack>\n )\n })}\n </Stack>\n <DialogFooter>\n <Button type=\"button\" variant=\"ghost\" onClick={onClose} disabled={pendingKey !== null}>\n {translate('common.cancel')}\n </Button>\n </DialogFooter>\n </>\n )\n}\n\n/** One selectable transfer target row — a new draft, or an existing document to append into. */\nfunction TransferTargetRow({\n testId,\n icon,\n label,\n pending,\n disabled,\n onClick,\n}: {\n testId: string\n icon: ReactNode\n label: string\n pending: boolean\n disabled: boolean\n onClick: () => void\n}) {\n return (\n <button\n type=\"button\"\n data-testid={testId}\n disabled={disabled}\n onClick={onClick}\n className={cn(\n 'flex items-center gap-3 rounded-md border border-border p-3 text-left transition-colors hover:border-primary/40 hover:bg-muted',\n disabled && 'cursor-not-allowed opacity-60',\n )}\n >\n <span className=\"grid size-8 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground\">\n {pending ? <Loader2 className=\"size-4 animate-spin\" /> : icon}\n </span>\n <Text size=\"sm\" weight=\"medium\" className=\"min-w-0 flex-1 truncate\">\n {label}\n </Text>\n </button>\n )\n}\n"]}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { AssetRecord, AssetRepository, BlobStorageAdapter, NewAssetInput } from '@elytracms/core/persistence';
|
|
2
|
+
import type { GeneratedVariants, ThumbSource, VariantWant } from './thumbnail.js';
|
|
3
|
+
/**
|
|
4
|
+
* The assets that still need at least one variant (pure, node-tested idempotence
|
|
5
|
+
* core, EC-420 AD-4): an image record missing a needed thumb OR a needed lqip.
|
|
6
|
+
* Records already carrying both, non-images, SVGs, and (for the thumb) already-
|
|
7
|
+
* small images are silently excluded — re-running on a done library is a no-op.
|
|
8
|
+
*/
|
|
9
|
+
export declare function assetsNeedingVariants(assets: readonly AssetRecord[]): AssetRecord[];
|
|
10
|
+
/** The variants an asset is missing — what the per-variant backfill asks the encoder for. */
|
|
11
|
+
export declare function variantWantFor(asset: AssetRecord): VariantWant;
|
|
12
|
+
/**
|
|
13
|
+
* The full upsert input that re-states an existing record with freshly generated
|
|
14
|
+
* variants attached — the targeted patch path for the backfill, within the
|
|
15
|
+
* existing repository/`assets.upsert` contract (EC-400 AD-4 / EC-420). Mirrors
|
|
16
|
+
* `assetMetadataUpsertInput`: every current column is restated so the full-replace
|
|
17
|
+
* upsert preserves it. A newly generated variant overlays; a variant the record
|
|
18
|
+
* ALREADY had (the other half of a per-variant fill) is preserved from `current`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function assetVariantUpsertInput(current: AssetRecord, variants: GeneratedVariants): NewAssetInput;
|
|
21
|
+
/** Live tally the "Generate previews" UI renders (EC-400 AD-1). */
|
|
22
|
+
export interface BackfillProgress {
|
|
23
|
+
/** How many assets need at least one variant (the candidate set size). */
|
|
24
|
+
total: number;
|
|
25
|
+
/** How many candidates have been processed (generated + skipped + failed). */
|
|
26
|
+
done: number;
|
|
27
|
+
/** How many candidates had at least one variant generated and patched in. */
|
|
28
|
+
generated: number;
|
|
29
|
+
/** Candidates the re-plan/encode declined (e.g. undecodable) — silent, valid. */
|
|
30
|
+
skipped: number;
|
|
31
|
+
/** Candidates whose fetch/encode/patch errored — surfaced, not fatal. */
|
|
32
|
+
failed: number;
|
|
33
|
+
/** Filename of the asset currently in flight (for the progress label). */
|
|
34
|
+
current?: string;
|
|
35
|
+
/** Set once the run stops early because the caller cancelled. */
|
|
36
|
+
cancelled?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** A mutable cancellation flag the UI flips to stop the sequential run (AD-1). */
|
|
39
|
+
export interface BackfillSignal {
|
|
40
|
+
cancelled: boolean;
|
|
41
|
+
}
|
|
42
|
+
export interface BackfillDeps {
|
|
43
|
+
/** Metadata reads + the variant patch write (the operations facade in the studio). */
|
|
44
|
+
assets: AssetRepository;
|
|
45
|
+
/** Blob boundary: fetch each original's bytes, store the thumb variant. */
|
|
46
|
+
blobs: BlobStorageAdapter;
|
|
47
|
+
/**
|
|
48
|
+
* Injectable variant producer; defaults to {@link generateVariants} over `blobs`.
|
|
49
|
+
* Receives the per-asset `want` so it produces ONLY the missing variants. Tests
|
|
50
|
+
* pass a deterministic fake so the iterator runs in node.
|
|
51
|
+
*/
|
|
52
|
+
generate?: (source: Blob, meta: ThumbSource, want: VariantWant) => Promise<GeneratedVariants>;
|
|
53
|
+
/** Fired with each patched record so the caller can update its cache in place. */
|
|
54
|
+
onGenerated?: (asset: AssetRecord) => void;
|
|
55
|
+
/** Fired after each candidate (and once at the end) to drive the progress UI. */
|
|
56
|
+
onProgress?: (progress: BackfillProgress) => void;
|
|
57
|
+
/** Cooperative cancellation — checked before each candidate (AD-1). */
|
|
58
|
+
signal?: BackfillSignal;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Run the backfill sequentially over a project's images that still need a variant.
|
|
62
|
+
* Resolves with the final tally; never throws (per-asset failures are counted, not
|
|
63
|
+
* fatal). One fetch per candidate serves both variants; only the missing ones are
|
|
64
|
+
* (re)generated (EC-420 AD-4), so an asset with a thumb but no lqip is patched with
|
|
65
|
+
* just the lqip and its existing thumb blob is left untouched.
|
|
66
|
+
*/
|
|
67
|
+
export declare function runVariantBackfill(deps: BackfillDeps, projectId: string): Promise<BackfillProgress>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { generateVariants, planLqip, planThumb } from './thumbnail.js';
|
|
2
|
+
/**
|
|
3
|
+
* The assets-rail "Generate previews" backfill (EC-400 AD-1 + EC-420 AD-4).
|
|
4
|
+
* Existing libraries predate the upload-time previews, so an explicit,
|
|
5
|
+
* deterministic maintenance action fetches each original ONCE, runs the SAME
|
|
6
|
+
* decode path as upload, and patches the record. It is idempotent PER VARIANT
|
|
7
|
+
* (EC-420 AD-4): an asset needing only the lqip gets one fetch of the original
|
|
8
|
+
* and fills just what's missing; an asset that already has both is skipped;
|
|
9
|
+
* non-images / SVGs / (for the thumb) already-small images never qualify. The run
|
|
10
|
+
* is sequential with visible progress and cancellable. Opportunistic per-open
|
|
11
|
+
* generation was REJECTED (it hides egress in random editor sessions and is
|
|
12
|
+
* untestable).
|
|
13
|
+
*
|
|
14
|
+
* The write goes through the injected {@link AssetRepository} — in the studio the
|
|
15
|
+
* EC-141 operations facade — so the patch flows through the one operations layer /
|
|
16
|
+
* TanStack Query cache (AD-5 / AD-17), never a direct backend client. The pure
|
|
17
|
+
* cores ({@link assetsNeedingVariants}, {@link assetVariantUpsertInput}) are
|
|
18
|
+
* node-tested; the byte fetch + canvas encode stay in the injectable shell.
|
|
19
|
+
*/
|
|
20
|
+
/** An image record still missing a thumb whose {@link planThumb} decision is `encode`. */
|
|
21
|
+
function needsThumb(asset) {
|
|
22
|
+
return asset.thumb === undefined && planThumb(asset).kind === 'encode';
|
|
23
|
+
}
|
|
24
|
+
/** An image record still missing an lqip whose {@link planLqip} decision is `encode`. */
|
|
25
|
+
function needsLqip(asset) {
|
|
26
|
+
return asset.lqip === undefined && planLqip(asset).kind === 'encode';
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The assets that still need at least one variant (pure, node-tested idempotence
|
|
30
|
+
* core, EC-420 AD-4): an image record missing a needed thumb OR a needed lqip.
|
|
31
|
+
* Records already carrying both, non-images, SVGs, and (for the thumb) already-
|
|
32
|
+
* small images are silently excluded — re-running on a done library is a no-op.
|
|
33
|
+
*/
|
|
34
|
+
export function assetsNeedingVariants(assets) {
|
|
35
|
+
return assets.filter((asset) => needsThumb(asset) || needsLqip(asset));
|
|
36
|
+
}
|
|
37
|
+
/** The variants an asset is missing — what the per-variant backfill asks the encoder for. */
|
|
38
|
+
export function variantWantFor(asset) {
|
|
39
|
+
return { thumb: needsThumb(asset), lqip: needsLqip(asset) };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The full upsert input that re-states an existing record with freshly generated
|
|
43
|
+
* variants attached — the targeted patch path for the backfill, within the
|
|
44
|
+
* existing repository/`assets.upsert` contract (EC-400 AD-4 / EC-420). Mirrors
|
|
45
|
+
* `assetMetadataUpsertInput`: every current column is restated so the full-replace
|
|
46
|
+
* upsert preserves it. A newly generated variant overlays; a variant the record
|
|
47
|
+
* ALREADY had (the other half of a per-variant fill) is preserved from `current`.
|
|
48
|
+
*/
|
|
49
|
+
export function assetVariantUpsertInput(current, variants) {
|
|
50
|
+
const thumb = variants.thumb ?? current.thumb;
|
|
51
|
+
const lqip = variants.lqip ?? current.lqip;
|
|
52
|
+
return {
|
|
53
|
+
id: current.id,
|
|
54
|
+
filename: current.filename,
|
|
55
|
+
contentType: current.contentType,
|
|
56
|
+
byteSize: current.byteSize,
|
|
57
|
+
storageKey: current.storageKey,
|
|
58
|
+
...(current.width !== undefined ? { width: current.width } : {}),
|
|
59
|
+
...(current.height !== undefined ? { height: current.height } : {}),
|
|
60
|
+
...(current.url !== undefined ? { url: current.url } : {}),
|
|
61
|
+
...(current.alt !== undefined ? { alt: current.alt } : {}),
|
|
62
|
+
...(current.title !== undefined ? { title: current.title } : {}),
|
|
63
|
+
...(current.focalPoint !== undefined ? { focalPoint: current.focalPoint } : {}),
|
|
64
|
+
...(current.meta !== undefined ? { meta: current.meta } : {}),
|
|
65
|
+
...(thumb !== undefined ? { thumb } : {}),
|
|
66
|
+
...(lqip !== undefined ? { lqip } : {}),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Run the backfill sequentially over a project's images that still need a variant.
|
|
71
|
+
* Resolves with the final tally; never throws (per-asset failures are counted, not
|
|
72
|
+
* fatal). One fetch per candidate serves both variants; only the missing ones are
|
|
73
|
+
* (re)generated (EC-420 AD-4), so an asset with a thumb but no lqip is patched with
|
|
74
|
+
* just the lqip and its existing thumb blob is left untouched.
|
|
75
|
+
*/
|
|
76
|
+
export async function runVariantBackfill(deps, projectId) {
|
|
77
|
+
const all = await deps.assets.listAssets(projectId);
|
|
78
|
+
const candidates = assetsNeedingVariants(all);
|
|
79
|
+
const generate = deps.generate ??
|
|
80
|
+
((source, meta, want) => generateVariants({ blobs: deps.blobs }, source, meta, want));
|
|
81
|
+
const progress = { total: candidates.length, done: 0, generated: 0, skipped: 0, failed: 0 };
|
|
82
|
+
deps.onProgress?.({ ...progress });
|
|
83
|
+
for (const asset of candidates) {
|
|
84
|
+
if (deps.signal?.cancelled) {
|
|
85
|
+
progress.cancelled = true;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
deps.onProgress?.({ ...progress, current: asset.filename });
|
|
89
|
+
try {
|
|
90
|
+
const bytes = await deps.blobs.get(asset.storageKey);
|
|
91
|
+
if (!bytes) {
|
|
92
|
+
progress.failed += 1;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const source = new Blob([bytes], { type: asset.contentType });
|
|
96
|
+
const variants = await generate(source, {
|
|
97
|
+
contentType: asset.contentType,
|
|
98
|
+
...(asset.width !== undefined ? { width: asset.width } : {}),
|
|
99
|
+
...(asset.height !== undefined ? { height: asset.height } : {}),
|
|
100
|
+
}, variantWantFor(asset));
|
|
101
|
+
if (variants.thumb === undefined && variants.lqip === undefined) {
|
|
102
|
+
progress.skipped += 1;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
const patched = await deps.assets.upsertAsset(projectId, assetVariantUpsertInput(asset, variants));
|
|
106
|
+
deps.onGenerated?.(patched);
|
|
107
|
+
progress.generated += 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
progress.failed += 1;
|
|
113
|
+
}
|
|
114
|
+
progress.done += 1;
|
|
115
|
+
deps.onProgress?.({ ...progress });
|
|
116
|
+
}
|
|
117
|
+
deps.onProgress?.({ ...progress });
|
|
118
|
+
return progress;
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=backfill-thumbs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backfill-thumbs.js","sourceRoot":"","sources":["../../../src/lib/assets/backfill-thumbs.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n AssetRecord,\n AssetRepository,\n BlobStorageAdapter,\n NewAssetInput,\n} from '@elytracms/core/persistence'\nimport { generateVariants, planLqip, planThumb } from './thumbnail'\nimport type { GeneratedVariants, ThumbSource, VariantWant } from './thumbnail'\n\n/**\n * The assets-rail \"Generate previews\" backfill (EC-400 AD-1 + EC-420 AD-4).\n * Existing libraries predate the upload-time previews, so an explicit,\n * deterministic maintenance action fetches each original ONCE, runs the SAME\n * decode path as upload, and patches the record. It is idempotent PER VARIANT\n * (EC-420 AD-4): an asset needing only the lqip gets one fetch of the original\n * and fills just what's missing; an asset that already has both is skipped;\n * non-images / SVGs / (for the thumb) already-small images never qualify. The run\n * is sequential with visible progress and cancellable. Opportunistic per-open\n * generation was REJECTED (it hides egress in random editor sessions and is\n * untestable).\n *\n * The write goes through the injected {@link AssetRepository} — in the studio the\n * EC-141 operations facade — so the patch flows through the one operations layer /\n * TanStack Query cache (AD-5 / AD-17), never a direct backend client. The pure\n * cores ({@link assetsNeedingVariants}, {@link assetVariantUpsertInput}) are\n * node-tested; the byte fetch + canvas encode stay in the injectable shell.\n */\n\n/** An image record still missing a thumb whose {@link planThumb} decision is `encode`. */\nfunction needsThumb(asset: AssetRecord): boolean {\n return asset.thumb === undefined && planThumb(asset).kind === 'encode'\n}\n\n/** An image record still missing an lqip whose {@link planLqip} decision is `encode`. */\nfunction needsLqip(asset: AssetRecord): boolean {\n return asset.lqip === undefined && planLqip(asset).kind === 'encode'\n}\n\n/**\n * The assets that still need at least one variant (pure, node-tested idempotence\n * core, EC-420 AD-4): an image record missing a needed thumb OR a needed lqip.\n * Records already carrying both, non-images, SVGs, and (for the thumb) already-\n * small images are silently excluded — re-running on a done library is a no-op.\n */\nexport function assetsNeedingVariants(assets: readonly AssetRecord[]): AssetRecord[] {\n return assets.filter((asset) => needsThumb(asset) || needsLqip(asset))\n}\n\n/** The variants an asset is missing — what the per-variant backfill asks the encoder for. */\nexport function variantWantFor(asset: AssetRecord): VariantWant {\n return { thumb: needsThumb(asset), lqip: needsLqip(asset) }\n}\n\n/**\n * The full upsert input that re-states an existing record with freshly generated\n * variants attached — the targeted patch path for the backfill, within the\n * existing repository/`assets.upsert` contract (EC-400 AD-4 / EC-420). Mirrors\n * `assetMetadataUpsertInput`: every current column is restated so the full-replace\n * upsert preserves it. A newly generated variant overlays; a variant the record\n * ALREADY had (the other half of a per-variant fill) is preserved from `current`.\n */\nexport function assetVariantUpsertInput(\n current: AssetRecord,\n variants: GeneratedVariants,\n): NewAssetInput {\n const thumb = variants.thumb ?? current.thumb\n const lqip = variants.lqip ?? current.lqip\n return {\n id: current.id,\n filename: current.filename,\n contentType: current.contentType,\n byteSize: current.byteSize,\n storageKey: current.storageKey,\n ...(current.width !== undefined ? { width: current.width } : {}),\n ...(current.height !== undefined ? { height: current.height } : {}),\n ...(current.url !== undefined ? { url: current.url } : {}),\n ...(current.alt !== undefined ? { alt: current.alt } : {}),\n ...(current.title !== undefined ? { title: current.title } : {}),\n ...(current.focalPoint !== undefined ? { focalPoint: current.focalPoint } : {}),\n ...(current.meta !== undefined ? { meta: current.meta } : {}),\n ...(thumb !== undefined ? { thumb } : {}),\n ...(lqip !== undefined ? { lqip } : {}),\n }\n}\n\n/** Live tally the \"Generate previews\" UI renders (EC-400 AD-1). */\nexport interface BackfillProgress {\n /** How many assets need at least one variant (the candidate set size). */\n total: number\n /** How many candidates have been processed (generated + skipped + failed). */\n done: number\n /** How many candidates had at least one variant generated and patched in. */\n generated: number\n /** Candidates the re-plan/encode declined (e.g. undecodable) — silent, valid. */\n skipped: number\n /** Candidates whose fetch/encode/patch errored — surfaced, not fatal. */\n failed: number\n /** Filename of the asset currently in flight (for the progress label). */\n current?: string\n /** Set once the run stops early because the caller cancelled. */\n cancelled?: boolean\n}\n\n/** A mutable cancellation flag the UI flips to stop the sequential run (AD-1). */\nexport interface BackfillSignal {\n cancelled: boolean\n}\n\nexport interface BackfillDeps {\n /** Metadata reads + the variant patch write (the operations facade in the studio). */\n assets: AssetRepository\n /** Blob boundary: fetch each original's bytes, store the thumb variant. */\n blobs: BlobStorageAdapter\n /**\n * Injectable variant producer; defaults to {@link generateVariants} over `blobs`.\n * Receives the per-asset `want` so it produces ONLY the missing variants. Tests\n * pass a deterministic fake so the iterator runs in node.\n */\n generate?: (source: Blob, meta: ThumbSource, want: VariantWant) => Promise<GeneratedVariants>\n /** Fired with each patched record so the caller can update its cache in place. */\n onGenerated?: (asset: AssetRecord) => void\n /** Fired after each candidate (and once at the end) to drive the progress UI. */\n onProgress?: (progress: BackfillProgress) => void\n /** Cooperative cancellation — checked before each candidate (AD-1). */\n signal?: BackfillSignal\n}\n\n/**\n * Run the backfill sequentially over a project's images that still need a variant.\n * Resolves with the final tally; never throws (per-asset failures are counted, not\n * fatal). One fetch per candidate serves both variants; only the missing ones are\n * (re)generated (EC-420 AD-4), so an asset with a thumb but no lqip is patched with\n * just the lqip and its existing thumb blob is left untouched.\n */\nexport async function runVariantBackfill(\n deps: BackfillDeps,\n projectId: string,\n): Promise<BackfillProgress> {\n const all = await deps.assets.listAssets(projectId)\n const candidates = assetsNeedingVariants(all)\n const generate =\n deps.generate ??\n ((source: Blob, meta: ThumbSource, want: VariantWant) =>\n generateVariants({ blobs: deps.blobs }, source, meta, want))\n\n const progress: BackfillProgress = { total: candidates.length, done: 0, generated: 0, skipped: 0, failed: 0 }\n deps.onProgress?.({ ...progress })\n\n for (const asset of candidates) {\n if (deps.signal?.cancelled) {\n progress.cancelled = true\n break\n }\n deps.onProgress?.({ ...progress, current: asset.filename })\n try {\n const bytes = await deps.blobs.get(asset.storageKey)\n if (!bytes) {\n progress.failed += 1\n } else {\n const source = new Blob([bytes as BlobPart], { type: asset.contentType })\n const variants = await generate(\n source,\n {\n contentType: asset.contentType,\n ...(asset.width !== undefined ? { width: asset.width } : {}),\n ...(asset.height !== undefined ? { height: asset.height } : {}),\n },\n variantWantFor(asset),\n )\n if (variants.thumb === undefined && variants.lqip === undefined) {\n progress.skipped += 1\n } else {\n const patched = await deps.assets.upsertAsset(projectId, assetVariantUpsertInput(asset, variants))\n deps.onGenerated?.(patched)\n progress.generated += 1\n }\n }\n } catch {\n progress.failed += 1\n }\n progress.done += 1\n deps.onProgress?.({ ...progress })\n }\n\n deps.onProgress?.({ ...progress })\n return progress\n}\n"]}
|
|
@@ -6,7 +6,8 @@ import type { AssetRepository, BlobStorageAdapter, ReferenceIndexRepository } fr
|
|
|
6
6
|
* `{ type: 'asset' }` target) — index-accurate, replacing the interim string
|
|
7
7
|
* scan (`find-asset-usages`), which could miss rich-text asset hrefs and
|
|
8
8
|
* false-positive on unrelated strings. Deleting an unused asset removes bytes
|
|
9
|
-
* (blob) AND the
|
|
9
|
+
* (blob) — original AND the EC-400 thumb variant blob — plus the metadata
|
|
10
|
+
* record. Results are structured data — never throws.
|
|
10
11
|
*/
|
|
11
12
|
export interface DeleteAssetDeps {
|
|
12
13
|
assets: AssetRepository;
|
|
@@ -15,6 +15,18 @@ export async function deleteAsset(deps, projectId, assetId, options = {}) {
|
|
|
15
15
|
// Bytes first, metadata last: a failure midway leaves a visible metadata
|
|
16
16
|
// record (re-deletable) rather than invisible orphaned bytes.
|
|
17
17
|
await deps.blobs.delete(asset.storageKey);
|
|
18
|
+
// EC-400 (AD-4): the downscaled preview is a SECOND stored blob — delete it
|
|
19
|
+
// too. Best-effort: a leftover thumb blob is harmless wasted storage (its
|
|
20
|
+
// metadata is going), so a thumb-delete failure must not block removing the
|
|
21
|
+
// asset the way an orphaned original would.
|
|
22
|
+
if (asset.thumb) {
|
|
23
|
+
try {
|
|
24
|
+
await deps.blobs.delete(asset.thumb.storageKey);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Swallow — the asset delete proceeds; the orphaned thumb blob is inert.
|
|
28
|
+
}
|
|
29
|
+
}
|
|
18
30
|
await deps.assets.removeAsset(projectId, assetId);
|
|
19
31
|
return { outcome: 'deleted' };
|
|
20
32
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"delete-asset.js","sourceRoot":"","sources":["../../../src/lib/assets/delete-asset.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAA;
|
|
1
|
+
{"version":3,"file":"delete-asset.js","sourceRoot":"","sources":["../../../src/lib/assets/delete-asset.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAA;AAqCjE,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,IAAqB,EACrB,SAAiB,EACjB,OAAe,EACf,UAA8B,EAAE;IAEhC,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;QAC7D,IAAI,CAAC,KAAK;YAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,CAAA;QAE3C,8EAA8E;QAC9E,iFAAiF;QACjF,mFAAmF;QACnF,IAAI,OAAO,CAAC,YAAY,KAAK,IAAI,EAAE,CAAC;YAClC,MAAM,UAAU,GAAG,MAAM,eAAe,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,CAAA;YAClE,IAAI,UAAU,GAAG,CAAC;gBAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,CAAA;QAC/D,CAAC;QAED,yEAAyE;QACzE,8DAA8D;QAC9D,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;QACzC,4EAA4E;QAC5E,0EAA0E;QAC1E,4EAA4E;QAC5E,4CAA4C;QAC5C,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;YAChB,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;YACjD,CAAC;YAAC,MAAM,CAAC;gBACP,yEAAyE;YAC3E,CAAC;QACH,CAAC;QACD,MAAM,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;QACjD,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,CAAA;IAC/B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,mBAAmB;YAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,CAAA;QACzE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAA;IAC9F,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,eAAe,CAC5B,IAAqB,EACrB,SAAiB,EACjB,OAAe;IAEf,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,CAAA;IACxF,MAAM,QAAQ,GAAG,IAAI,GAAG,CACtB,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,IAAI,IAAI,KAAK,CAAC,MAAM,CAAC,EAAE,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,CAChF,CAAA;IACD,OAAO,QAAQ,CAAC,IAAI,CAAA;AACtB,CAAC","sourcesContent":["import { RecordNotFoundError } from '@elytracms/core/persistence'\nimport type {\n AssetRepository,\n BlobStorageAdapter,\n ReferenceIndexRepository,\n} from '@elytracms/core/persistence'\n\n/**\n * Reference-respecting asset deletion (EC-151, reworked EC-202). Deleting an\n * asset that is referenced is blocked until explicitly confirmed; the usage\n * count comes from the **EC-155 reference index** (`references.inbound` for the\n * `{ type: 'asset' }` target) — index-accurate, replacing the interim string\n * scan (`find-asset-usages`), which could miss rich-text asset hrefs and\n * false-positive on unrelated strings. Deleting an unused asset removes bytes\n * (blob) — original AND the EC-400 thumb variant blob — plus the metadata\n * record. Results are structured data — never throws.\n */\n\nexport interface DeleteAssetDeps {\n assets: AssetRepository\n blobs: BlobStorageAdapter\n /** The EC-155 reference index — the accurate inbound-usage source (EC-202). */\n references: Pick<ReferenceIndexRepository, 'inbound'>\n}\n\nexport type DeleteAssetResult =\n | { outcome: 'deleted' }\n /** The asset is referenced and `confirmInUse` was not set: nothing was removed. */\n | { outcome: 'blocked'; usageCount: number }\n | { outcome: 'not-found' }\n | { outcome: 'error'; message: string }\n\nexport interface DeleteAssetOptions {\n /** Caller has seen the usage count and explicitly confirmed deletion. */\n confirmInUse?: boolean\n}\n\nexport async function deleteAsset(\n deps: DeleteAssetDeps,\n projectId: string,\n assetId: string,\n options: DeleteAssetOptions = {},\n): Promise<DeleteAssetResult> {\n try {\n const asset = await deps.assets.findAsset(projectId, assetId)\n if (!asset) return { outcome: 'not-found' }\n\n // Only consult the reference index when the caller has NOT already confirmed.\n // The delete dialog enumerates usages up-front and confirms with `confirmInUse`,\n // so a confirmed delete skips this redundant inbound query entirely (EC-301 perf).\n if (options.confirmInUse !== true) {\n const usageCount = await assetUsageCount(deps, projectId, assetId)\n if (usageCount > 0) return { outcome: 'blocked', usageCount }\n }\n\n // Bytes first, metadata last: a failure midway leaves a visible metadata\n // record (re-deletable) rather than invisible orphaned bytes.\n await deps.blobs.delete(asset.storageKey)\n // EC-400 (AD-4): the downscaled preview is a SECOND stored blob — delete it\n // too. Best-effort: a leftover thumb blob is harmless wasted storage (its\n // metadata is going), so a thumb-delete failure must not block removing the\n // asset the way an orphaned original would.\n if (asset.thumb) {\n try {\n await deps.blobs.delete(asset.thumb.storageKey)\n } catch {\n // Swallow — the asset delete proceeds; the orphaned thumb blob is inert.\n }\n }\n await deps.assets.removeAsset(projectId, assetId)\n return { outcome: 'deleted' }\n } catch (error) {\n if (error instanceof RecordNotFoundError) return { outcome: 'not-found' }\n return { outcome: 'error', message: error instanceof Error ? error.message : String(error) }\n }\n}\n\n/**\n * Distinct inbound usages of an asset from the reference index (EC-202). One\n * `source + path` (e.g. `post:hello · values/cover`) can carry both a draft and\n * a published index entry — that is a single usage, so variants collapse before\n * counting (the gate is \"is this referenced\", not \"how many index rows\").\n */\nasync function assetUsageCount(\n deps: DeleteAssetDeps,\n projectId: string,\n assetId: string,\n): Promise<number> {\n const inbound = await deps.references.inbound(projectId, { type: 'asset', id: assetId })\n const distinct = new Set(\n inbound.map((entry) => `${entry.source.type}:${entry.source.id}:${entry.path}`),\n )\n return distinct.size\n}\n"]}
|
package/dist/lib/assets/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/assets/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,cAAc,gBAAgB,CAAA;AAC9B,cAAc,oBAAoB,CAAA;AAClC,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA","sourcesContent":["/**\n * Asset upload, deletion, and usage logic (EC-151). Pure/injectable modules:\n * the studio UI consumes these; storage is reached only through the\n * `BlobStorageAdapter` boundary.\n */\nexport * from './upload-state'\nexport * from './image-dimensions'\nexport * from './upload'\nexport * from './delete-asset'\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/assets/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,cAAc,gBAAgB,CAAA;AAC9B,cAAc,oBAAoB,CAAA;AAClC,cAAc,aAAa,CAAA;AAC3B,cAAc,mBAAmB,CAAA;AACjC,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA","sourcesContent":["/**\n * Asset upload, deletion, and usage logic (EC-151). Pure/injectable modules:\n * the studio UI consumes these; storage is reached only through the\n * `BlobStorageAdapter` boundary.\n */\nexport * from './upload-state'\nexport * from './image-dimensions'\nexport * from './thumbnail'\nexport * from './backfill-thumbs'\nexport * from './upload'\nexport * from './delete-asset'\n"]}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { AssetThumbVariant, BlobStorageAdapter } from '@elytracms/core/persistence';
|
|
2
|
+
/**
|
|
3
|
+
* Client-side preview generation (EC-400 thumb + EC-420 LQIP). Every studio image
|
|
4
|
+
* surface used to stream the full-size Convex serve URL in a plain `<img>`, so a
|
|
5
|
+
* page of big originals cost heavy egress. At the moment an upload already has the
|
|
6
|
+
* bytes in hand — and again from the assets rail's "Generate previews" backfill —
|
|
7
|
+
* ONE `createImageBitmap` decode yields BOTH variants:
|
|
8
|
+
*
|
|
9
|
+
* - **thumb** (EC-400): a ≤480px-long-edge re-encode stored as a SECOND blob;
|
|
10
|
+
* editorial-only (studio grids/pickers/table cells render it instead of the
|
|
11
|
+
* original). NEVER leaves the studio.
|
|
12
|
+
* - **lqip** (EC-420): a ≤24px, low-quality re-encode base64'd into a tiny `data:`
|
|
13
|
+
* URI (hard-capped at {@link ASSET_LQIP_MAX_LENGTH} chars) and stored INLINE on
|
|
14
|
+
* the record. Unlike the thumb it rides the delivery wire (AD-2): the
|
|
15
|
+
* `@elytracms/next` image seam feeds it to `next/image` as `placeholder="blur"`.
|
|
16
|
+
*
|
|
17
|
+
* The decisions (`planThumb` / `planLqip`) are pure, node-testable cores; the
|
|
18
|
+
* canvas re-encode (`canvasVariantEncoder`) is a thin, injectable DOM shell that
|
|
19
|
+
* decodes once and draws each requested target; the orchestrator
|
|
20
|
+
* (`generateVariants`) plans → encodes (one decode) → stores the thumb blob +
|
|
21
|
+
* builds the lqip data URI, and is testable in node with a fake encoder over an
|
|
22
|
+
* in-memory blob store.
|
|
23
|
+
*
|
|
24
|
+
* Ratified spec: thumb ≤480px, quality ~0.8 (EC-400 AD-2). lqip ≤24px, quality
|
|
25
|
+
* ~0.45 (EC-420 AD-1); UNLIKE the thumb it is generated even for images already
|
|
26
|
+
* ≤480px (any raster benefits from blur-up). Both prefer `image/webp` with an
|
|
27
|
+
* `image/jpeg` fallback (the ACTUAL returned blob type is recorded — Safari has
|
|
28
|
+
* historically ignored the requested type). Skips are silent and valid — not
|
|
29
|
+
* validation issues; a generation failure never fails the upload.
|
|
30
|
+
*/
|
|
31
|
+
/** The ratified maximum thumbnail long-edge, in pixels (EC-400 AD-2). */
|
|
32
|
+
export declare const THUMB_MAX_EDGE = 480;
|
|
33
|
+
/** The ratified re-encode quality for the lossy thumbnail codecs (EC-400 AD-2). */
|
|
34
|
+
export declare const THUMB_QUALITY = 0.8;
|
|
35
|
+
/** The ratified maximum LQIP long-edge, in pixels (EC-420 AD-1). */
|
|
36
|
+
export declare const LQIP_MAX_EDGE = 24;
|
|
37
|
+
/** The ratified re-encode quality for the LQIP placeholder (EC-420 AD-1). */
|
|
38
|
+
export declare const LQIP_QUALITY = 0.45;
|
|
39
|
+
/** The re-encode target size a variant needs, or the reason it is skipped. */
|
|
40
|
+
export type ThumbPlan = {
|
|
41
|
+
kind: 'skip';
|
|
42
|
+
reason: 'non-image' | 'vector' | 'no-dimensions' | 'already-small';
|
|
43
|
+
} | {
|
|
44
|
+
kind: 'encode';
|
|
45
|
+
width: number;
|
|
46
|
+
height: number;
|
|
47
|
+
};
|
|
48
|
+
/** The LQIP plan — like {@link ThumbPlan} but WITHOUT the `already-small` skip (AD-3). */
|
|
49
|
+
export type LqipPlan = {
|
|
50
|
+
kind: 'skip';
|
|
51
|
+
reason: 'non-image' | 'vector' | 'no-dimensions';
|
|
52
|
+
} | {
|
|
53
|
+
kind: 'encode';
|
|
54
|
+
width: number;
|
|
55
|
+
height: number;
|
|
56
|
+
};
|
|
57
|
+
/** The metadata `planThumb` / `planLqip` and the encoders reason over. */
|
|
58
|
+
export interface ThumbSource {
|
|
59
|
+
contentType: string;
|
|
60
|
+
width?: number;
|
|
61
|
+
height?: number;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Decide whether an asset needs a downscaled thumb, and to what size (EC-400
|
|
65
|
+
* AD-2/AD-3). Pure and deterministic — the node-tested core of the skip rules.
|
|
66
|
+
* A returned `encode` plan preserves aspect ratio, fitting the long edge to
|
|
67
|
+
* {@link THUMB_MAX_EDGE}; the short edge rounds to at least one pixel.
|
|
68
|
+
*/
|
|
69
|
+
export declare function planThumb(source: ThumbSource): ThumbPlan;
|
|
70
|
+
/**
|
|
71
|
+
* Decide whether an asset needs an LQIP blur placeholder, and to what size (EC-420
|
|
72
|
+
* AD-1/AD-3). Pure and deterministic. Mirrors {@link planThumb}'s skips (non-image,
|
|
73
|
+
* SVG, no dimensions) but — UNLIKE the thumb — has NO `already-small` skip: any
|
|
74
|
+
* raster benefits from delivery blur-up, so even a ≤480px image gets one (fit to
|
|
75
|
+
* {@link LQIP_MAX_EDGE}, never upscaled).
|
|
76
|
+
*/
|
|
77
|
+
export declare function planLqip(source: ThumbSource): LqipPlan;
|
|
78
|
+
/** The bytes + ACTUAL mime type + dimensions an encoder produced for one variant. */
|
|
79
|
+
export interface EncodedVariant {
|
|
80
|
+
bytes: Uint8Array;
|
|
81
|
+
/** The blob's real type — NOT necessarily the requested one (AD-2 Safari note). */
|
|
82
|
+
contentType: string;
|
|
83
|
+
width: number;
|
|
84
|
+
height: number;
|
|
85
|
+
}
|
|
86
|
+
/** A re-encode target the encoder draws into (per-variant quality). */
|
|
87
|
+
export interface EncodeTarget {
|
|
88
|
+
width: number;
|
|
89
|
+
height: number;
|
|
90
|
+
quality: number;
|
|
91
|
+
}
|
|
92
|
+
/** Which variants to draw+encode from the single decode, at their target sizes. */
|
|
93
|
+
export interface VariantTargets {
|
|
94
|
+
thumb?: EncodeTarget;
|
|
95
|
+
lqip?: EncodeTarget;
|
|
96
|
+
}
|
|
97
|
+
/** The encoded output of one decode: whichever requested variants encoded. */
|
|
98
|
+
export interface EncodedVariants {
|
|
99
|
+
thumb?: EncodedVariant;
|
|
100
|
+
lqip?: EncodedVariant;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Turn source image bytes into downscaled preview bytes for BOTH variants from a
|
|
104
|
+
* single decode. Returns whichever variants encoded (an empty object when the
|
|
105
|
+
* source cannot be decoded/encoded) — never throws; the caller treats a missing
|
|
106
|
+
* variant as "no preview", never a failure. Injectable so the orchestrator + the
|
|
107
|
+
* upload/backfill flows unit-test in node without a browser.
|
|
108
|
+
*/
|
|
109
|
+
export type VariantEncoder = (source: Blob, targets: VariantTargets) => Promise<EncodedVariants>;
|
|
110
|
+
/**
|
|
111
|
+
* The default browser encoder: ONE `createImageBitmap` decode → per-variant 2D
|
|
112
|
+
* canvas → `toBlob` (EC-420: the same decode yields thumb AND lqip). A thin DOM
|
|
113
|
+
* shell, deliberately untested in node (the orchestrator is tested with a fake
|
|
114
|
+
* encoder). Any failure resolves an empty object, so a preview never breaks an
|
|
115
|
+
* upload.
|
|
116
|
+
*/
|
|
117
|
+
export declare const canvasVariantEncoder: VariantEncoder;
|
|
118
|
+
/** Blob storage + the (optionally injected) encoder the orchestrator needs. */
|
|
119
|
+
export interface VariantGenerationDeps {
|
|
120
|
+
blobs: BlobStorageAdapter;
|
|
121
|
+
/** Defaults to {@link canvasVariantEncoder}; tests inject a deterministic fake. */
|
|
122
|
+
encode?: VariantEncoder;
|
|
123
|
+
}
|
|
124
|
+
/** Which variants the orchestrator should PRODUCE (backfill fills only the missing ones). */
|
|
125
|
+
export interface VariantWant {
|
|
126
|
+
thumb?: boolean;
|
|
127
|
+
lqip?: boolean;
|
|
128
|
+
}
|
|
129
|
+
/** The stored/inlined variants a generation run produced (either may be absent). */
|
|
130
|
+
export interface GeneratedVariants {
|
|
131
|
+
/** The stored downscaled thumb (a second blob's key/url + dims), or absent. */
|
|
132
|
+
thumb?: AssetThumbVariant;
|
|
133
|
+
/** The LQIP blur-placeholder `data:` URI (≤ cap), or absent. */
|
|
134
|
+
lqip?: string;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Plan → encode (ONE decode) → store/inline BOTH variants (EC-400 + EC-420). The
|
|
138
|
+
* thumb rides a SECOND `store()` call (the same blob boundary the upload uses); the
|
|
139
|
+
* lqip is inlined as a capped base64 `data:` URI needing no blob store at all.
|
|
140
|
+
* `want` narrows which variants to produce — the per-variant backfill (AD-4) asks
|
|
141
|
+
* for only the missing one so an existing thumb/lqip is never regenerated. Returns
|
|
142
|
+
* whichever variants succeeded; any skip/failure simply omits that variant (never
|
|
143
|
+
* throws — a preview must never fail an upload).
|
|
144
|
+
*/
|
|
145
|
+
export declare function generateVariants(deps: VariantGenerationDeps, source: Blob, meta: ThumbSource, want?: VariantWant): Promise<GeneratedVariants>;
|