@elytracms/studio 0.0.95 → 0.0.97

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.
Files changed (104) hide show
  1. package/convex/cms.ts +272 -16
  2. package/convex/delivery-folded-drift.test.ts +104 -0
  3. package/convex/delivery.ts +24 -8
  4. package/convex/depth-codec-lookalike.test.ts +192 -0
  5. package/convex/fixture-accounts.test.ts +305 -0
  6. package/convex/fixtureAccounts.ts +298 -0
  7. package/convex/incarnation.test.ts +149 -0
  8. package/convex/ingest.test.ts +145 -3
  9. package/convex/ingest.ts +123 -17
  10. package/convex/receipt-races.test.ts +184 -0
  11. package/convex/records-zod-copy.test.ts +76 -0
  12. package/convex/schema.ts +20 -1
  13. package/convex/strict-delete.test.ts +129 -0
  14. package/convex/strict-reference-race.test.ts +139 -0
  15. package/dist/components/ai/ai-chat-panel.js +15 -2
  16. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  17. package/dist/components/ai/translate-document-action.d.ts +6 -0
  18. package/dist/components/ai/translate-document-action.js.map +1 -1
  19. package/dist/components/ai/translate-document-dialog.d.ts +1 -1
  20. package/dist/components/ai/translate-document-dialog.js +5 -5
  21. package/dist/components/ai/translate-document-dialog.js.map +1 -1
  22. package/dist/components/assets/asset-detail.js +5 -2
  23. package/dist/components/assets/asset-detail.js.map +1 -1
  24. package/dist/components/assets/asset-locale.d.ts +7 -0
  25. package/dist/components/assets/asset-locale.js +16 -2
  26. package/dist/components/assets/asset-locale.js.map +1 -1
  27. package/dist/components/assets/asset-meta-drift-notice.d.ts +13 -0
  28. package/dist/components/assets/asset-meta-drift-notice.js +22 -0
  29. package/dist/components/assets/asset-meta-drift-notice.js.map +1 -0
  30. package/dist/components/assets/asset-meta-label.d.ts +2 -1
  31. package/dist/components/assets/asset-meta-label.js +4 -2
  32. package/dist/components/assets/asset-meta-label.js.map +1 -1
  33. package/dist/components/assets/asset-metadata.d.ts +33 -13
  34. package/dist/components/assets/asset-metadata.js +46 -22
  35. package/dist/components/assets/asset-metadata.js.map +1 -1
  36. package/dist/components/assets/asset-search.d.ts +6 -2
  37. package/dist/components/assets/asset-search.js +15 -5
  38. package/dist/components/assets/asset-search.js.map +1 -1
  39. package/dist/components/assets/assets-collection-view-source.d.ts +5 -3
  40. package/dist/components/assets/assets-collection-view-source.js +7 -5
  41. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  42. package/dist/components/builder/canvas-rebase.d.ts +36 -12
  43. package/dist/components/builder/canvas-rebase.js +13 -3
  44. package/dist/components/builder/canvas-rebase.js.map +1 -1
  45. package/dist/components/builder/canvas-render-context.js +5 -4
  46. package/dist/components/builder/canvas-render-context.js.map +1 -1
  47. package/dist/components/builder/composition-field-editor.d.ts +12 -6
  48. package/dist/components/builder/composition-field-editor.js +6 -5
  49. package/dist/components/builder/composition-field-editor.js.map +1 -1
  50. package/dist/components/cms/asset-picker-core.d.ts +2 -2
  51. package/dist/components/cms/asset-picker-core.js +2 -2
  52. package/dist/components/cms/asset-picker-core.js.map +1 -1
  53. package/dist/components/cms/asset-picker.js +3 -1
  54. package/dist/components/cms/asset-picker.js.map +1 -1
  55. package/dist/components/cms/canvas-commit.d.ts +6 -0
  56. package/dist/components/cms/canvas-commit.js +5 -1
  57. package/dist/components/cms/canvas-commit.js.map +1 -1
  58. package/dist/components/cms/cms-store.d.ts +24 -8
  59. package/dist/components/cms/cms-store.js +21 -14
  60. package/dist/components/cms/cms-store.js.map +1 -1
  61. package/dist/components/cms/collection-table.js +4 -11
  62. package/dist/components/cms/collection-table.js.map +1 -1
  63. package/dist/components/cms/detail-editor.js +2 -2
  64. package/dist/components/cms/detail-editor.js.map +1 -1
  65. package/dist/components/cms/draft-commit.d.ts +16 -3
  66. package/dist/components/cms/draft-commit.js +3 -2
  67. package/dist/components/cms/draft-commit.js.map +1 -1
  68. package/dist/components/cms/use-document-draft.d.ts +107 -10
  69. package/dist/components/cms/use-document-draft.js +157 -42
  70. package/dist/components/cms/use-document-draft.js.map +1 -1
  71. package/dist/components/collection-view/cms-collection-view-source.js +6 -4
  72. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  73. package/dist/components/components/transfer-dialog.js +6 -4
  74. package/dist/components/components/transfer-dialog.js.map +1 -1
  75. package/dist/lib/ai/chat-state.d.ts +7 -0
  76. package/dist/lib/ai/chat-state.js.map +1 -1
  77. package/dist/lib/ai/tools.d.ts +8 -0
  78. package/dist/lib/ai/tools.js +4 -0
  79. package/dist/lib/ai/tools.js.map +1 -1
  80. package/dist/lib/cms-views/view-model.d.ts +15 -8
  81. package/dist/lib/cms-views/view-model.js +19 -5
  82. package/dist/lib/cms-views/view-model.js.map +1 -1
  83. package/dist/lib/collection-view/__fixtures__/document-writes.js +1 -0
  84. package/dist/lib/collection-view/__fixtures__/document-writes.js.map +1 -1
  85. package/dist/lib/collection-view/collection-view-source.d.ts +18 -7
  86. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  87. package/dist/lib/data/collection-content.js +5 -0
  88. package/dist/lib/data/collection-content.js.map +1 -1
  89. package/dist/lib/engine-version.d.ts +1 -1
  90. package/dist/lib/engine-version.js +1 -1
  91. package/dist/lib/engine-version.js.map +1 -1
  92. package/dist/lib/i18n/messages.de.js +15 -1
  93. package/dist/lib/i18n/messages.de.js.map +1 -1
  94. package/dist/lib/i18n/messages.en.d.ts +13 -0
  95. package/dist/lib/i18n/messages.en.js +16 -1
  96. package/dist/lib/i18n/messages.en.js.map +1 -1
  97. package/dist/lib/operations/cms-persistence.d.ts +119 -35
  98. package/dist/lib/operations/cms-persistence.js +441 -91
  99. package/dist/lib/operations/cms-persistence.js.map +1 -1
  100. package/dist/lib/persistence/convex-adapter.js +80 -8
  101. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  102. package/dist/lib/persona/labels.js +2 -0
  103. package/dist/lib/persona/labels.js.map +1 -1
  104. package/package.json +4 -3
@@ -1 +1 @@
1
- {"version":3,"file":"asset-metadata.js","sourceRoot":"","sources":["../../../src/components/assets/asset-metadata.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CollectionDef, FieldDef } from '@elytracms/core/cms-core'\nimport { liftLegacyAlt, metaPatchBetween } from '@elytracms/core/persistence'\nimport type { AssetReadLocale, AssetRecord, AssetRepository, AssetWriteInput } from '@elytracms/core/persistence'\n\n/**\n * Asset metadata editing (EC-152). A patch is applied through the\n * {@link AssetRepository} interface — in the studio that is the EC-141\n * operations facade, so every edit runs the `assets.upsert` command\n * (validate → authorize → mutate → event). Pure orchestration; injectable\n * for tests.\n */\n\n/**\n * System columns the upload pipeline owns — never editable metadata, so they're\n * excluded from the asset collection's schema-driven forms: the image field's\n * options dialog (EC-301, `field-control.tsx`) and the assets rail's detail\n * surface (EC-319, `asset-detail.tsx`) share this ONE reserved set so neither\n * drifts from the other. Everything else a project declares on the `kind:'asset'`\n * collection round-trips through the record's open `meta` bag.\n */\nexport const SYSTEM_ASSET_FIELDS: ReadonlySet<string> = new Set([\n 'filename',\n 'url',\n 'contentType',\n 'byteSize',\n 'width',\n 'height',\n 'storageKey',\n 'title',\n])\n\n/**\n * The asset collection's EDITABLE metadata fields — declared fields minus the\n * system columns above and anything explicitly `readOnly` (EC-301/EC-319).\n */\nexport function assetMetaFieldsOf(collection: CollectionDef): FieldDef[] {\n return collection.fields.filter((f) => !SYSTEM_ASSET_FIELDS.has(f.name) && !f.form?.readOnly)\n}\n\n/** Drop `undefined` values so a persisted meta bag stays JSON/Convex-clean. */\nexport function pruneMeta(meta: Record<string, unknown>): Record<string, unknown> {\n return Object.fromEntries(Object.entries(meta).filter(([, v]) => v !== undefined))\n}\n\n/** Human-readable byte size (`1536` → `1.5 KB`). Shared by the asset grid + detail. */\nexport function formatBytes(byteSize: number): string {\n if (byteSize < 1024) return `${byteSize} B`\n if (byteSize < 1024 * 1024) return `${(byteSize / 1024).toFixed(1)} KB`\n return `${(byteSize / (1024 * 1024)).toFixed(1)} MB`\n}\n\n/**\n * An editor's metadata change, in the EC-577 write rule: an ABSENT key leaves the\n * stored value untouched, `null` clears it, a value sets it.\n */\nexport interface AssetMetadataPatch {\n /** New display title: a value sets (trimmed), `null` or a trimmed-empty string clears. */\n title?: string | null\n /**\n * New focal point (EC-248), the normalized 0–1 hotspot threaded to delivery's\n * `object-position` (EC-230). `{x,y}` sets it, `null` clears it (\"Reset to\n * centre\").\n */\n focalPoint?: { x: number; y: number } | null\n /**\n * A per-key change to the schema-driven metadata bag (EC-301 → EC-634): alt,\n * copyright, credit, … keyed by the asset collection's `fields`. A key with a value\n * sets it, `null` removes it, a key the patch does not name keeps its STORED value —\n * applied to the bag as stored, inside the write (core `resolveAssetWrite`), so an\n * editor never writes back a stale copy of a key another session changed. Build it\n * with {@link assetMetaDraftPatch} from the draft the editor opened and the one it holds.\n * `meta.alt` is the asset's ONE shared alt (EC-587) — there is no separate alt key. There\n * is no whole-bag form here: every studio editor of an asset's metadata patches per key.\n */\n metaPatch?: Record<string, unknown>\n /**\n * EC-632 — the same per-key change to the per-locale buckets of the asset collection's\n * `localized: true` fields: `{ [locale]: { [field]: value | null } }`, applied per locale\n * and key to the stored buckets inside the write.\n */\n localizedMetaPatch?: Record<string, Record<string, unknown>>\n}\n\n/**\n * An asset's metadata as an editor holds it (EC-632) — the DOCUMENT shape, so editing it\n * is the mechanism documents use: the shared bag (a document's `values`) and the\n * per-locale buckets of the `localized: true` fields (its `localized`). The form reads and\n * writes one field in one locale ({@link assetMetaFieldValue} / {@link setAssetMetaField});\n * the save is a structural diff of the whole draft ({@link assetMetaDraftPatch}), so it\n * needs no config and never writes a key or a locale the editor did not change.\n */\nexport interface AssetMetaDraft {\n readonly meta: Readonly<Record<string, unknown>>\n readonly localizedMeta: Readonly<Record<string, Readonly<Record<string, unknown>>>>\n}\n\n/** The fields of the asset collection an editor edits by — only the name and the locale flag matter here. */\ntype MetaField = Pick<FieldDef, 'name' | 'localized'>\n\n/**\n * The draft an editor opens (EC-587 + EC-632): the stored bags, with a legacy `alt` column\n * lifted into the shared one (core `liftLegacyAlt`), so a not-yet-migrated asset shows the\n * alt every reader shows — and its first save moves it home.\n */\nexport function assetMetaDraft(record: Pick<AssetRecord, 'alt' | 'meta' | 'localizedMeta'>): AssetMetaDraft {\n const localizedMeta: Record<string, Record<string, unknown>> = {}\n for (const [locale, bucket] of Object.entries(record.localizedMeta ?? {})) localizedMeta[locale] = { ...bucket }\n return { meta: liftLegacyAlt(record)?.meta ?? { ...(record.meta ?? {}) }, localizedMeta }\n}\n\nconst hasOwn = (bag: Readonly<Record<string, unknown>> | undefined, key: string): boolean =>\n bag !== undefined && Object.prototype.hasOwnProperty.call(bag, key)\n\n/**\n * What the form shows for one field (EC-632): a `localized: true` field the editor's\n * locale's own value (an untranslated locale shows empty — it falls back to the default\n * locale on delivery); the default locale also shows a value written before the field was\n * localized (the shared bag — delivery's last-resort fallback, so it IS that locale's\n * value). Any other field the shared value. Without a locale config, the shared value.\n */\nexport function assetMetaFieldValue(draft: AssetMetaDraft, field: MetaField, at: AssetReadLocale | null): unknown {\n if (field.localized === true && at !== null) {\n const bucket = draft.localizedMeta[at.locale]\n if (hasOwn(bucket, field.name)) return bucket![field.name]\n return at.locale === at.default ? draft.meta[field.name] : undefined\n }\n return draft.meta[field.name]\n}\n\n/**\n * Write one field of the draft (EC-632) — `undefined` removes the value:\n * - a `localized: true` field goes into the editor's locale's bucket; in the DEFAULT\n * locale a value written before the field was localized leaves the shared bag — the\n * default bucket is its home now, so a clear there really clears it;\n * - any other field goes into the shared bag, and a value a former localization left in a\n * bucket is removed (one home per key — a stale bucket would shadow the shared value);\n * - without a locale config (`at` null) a localized field writes the shared bag and leaves\n * its translations alone.\n */\nexport function setAssetMetaField(\n draft: AssetMetaDraft,\n field: MetaField,\n at: AssetReadLocale | null,\n value: unknown,\n): AssetMetaDraft {\n const name = field.name\n const withValue = (bag: Readonly<Record<string, unknown>> | undefined): Record<string, unknown> => {\n const next = { ...(bag ?? {}) }\n if (value === undefined) delete next[name]\n else next[name] = value\n return next\n }\n const without = (bag: Readonly<Record<string, unknown>>): Record<string, unknown> => {\n const next = { ...bag }\n delete next[name]\n return next\n }\n if (field.localized === true) {\n // No locale config installed yet: the shared bag, and the translations left untouched.\n if (at === null) return { meta: withValue(draft.meta), localizedMeta: draft.localizedMeta }\n const bucket = withValue(draft.localizedMeta[at.locale])\n const localizedMeta = { ...draft.localizedMeta, [at.locale]: bucket }\n const meta = at.locale === at.default && hasOwn(draft.meta, name) ? without(draft.meta) : draft.meta\n return { meta, localizedMeta }\n }\n const localizedMeta: Record<string, Readonly<Record<string, unknown>>> = {}\n for (const [locale, bucket] of Object.entries(draft.localizedMeta)) {\n localizedMeta[locale] = hasOwn(bucket, name) ? without(bucket) : bucket\n }\n return { meta: withValue(draft.meta), localizedMeta }\n}\n\n/**\n * The per-key, per-locale patch of a metadata edit (EC-634 × EC-632): what changed between\n * the draft the editor OPENED ({@link assetMetaDraft} at open, or the draft of its last\n * successful save) and the draft it holds — `undefined` values pruned, a removed key\n * `null`, a locale it did not touch absent. Every asset metadata editor (the image options\n * dialog, the asset library's detail, the assets rail) sends this instead of its buffer.\n */\nexport function assetMetaDraftPatch(\n opened: AssetMetaDraft,\n edited: AssetMetaDraft,\n): Pick<AssetMetadataPatch, 'metaPatch' | 'localizedMetaPatch'> {\n const metaPatch = metaPatchBetween(pruneMeta({ ...opened.meta }), pruneMeta({ ...edited.meta }))\n const localizedMetaPatch: Record<string, Record<string, unknown>> = {}\n for (const locale of new Set([...Object.keys(opened.localizedMeta), ...Object.keys(edited.localizedMeta)])) {\n const patch = metaPatchBetween(\n pruneMeta({ ...(opened.localizedMeta[locale] ?? {}) }),\n pruneMeta({ ...(edited.localizedMeta[locale] ?? {}) }),\n )\n if (Object.keys(patch).length > 0) localizedMetaPatch[locale] = patch\n }\n return {\n ...(Object.keys(metaPatch).length > 0 ? { metaPatch } : {}),\n ...(Object.keys(localizedMetaPatch).length > 0 ? { localizedMetaPatch } : {}),\n }\n}\n\n/** The form's flat view of a draft in one locale — `{ [field]: value }` for the given fields. */\nexport function assetMetaFormValues(\n draft: AssetMetaDraft,\n fields: readonly MetaField[],\n at: AssetReadLocale | null,\n): Record<string, unknown> {\n return Object.fromEntries(fields.map((field) => [field.name, assetMetaFieldValue(draft, field, at)]))\n}\n\n/** A text column's write: a trimmed value sets, `null`/empty clears. */\nfunction textWrite(value: string | null): string | null {\n const trimmed = value?.trim() ?? ''\n return trimmed.length > 0 ? trimmed : null\n}\n\n/**\n * The asset write for a metadata edit (EC-577): ONLY the keys the editor changed,\n * with `null` for a clear. It deliberately restates nothing else — not the\n * upload-owned columns, and never `thumb`/`lqip`: a stale copy of those would\n * overwrite a preview backfill that finished while the dialog was open, and a\n * clear expressed as an omitted key is exactly what never persisted on Convex.\n *\n * EC-634: `meta` travels as the per-key `metaPatch` — never the whole bag, which would\n * carry a stale copy of every key the editor did not change. An empty patch is not sent.\n * The legacy `alt` column (EC-587) is lifted into the stored bag and cleared by the write\n * rule itself, in the same transaction, whenever a patch is applied — so an alt the editor\n * did not touch survives, and one the editor cleared stays cleared (ASSETS D21).\n */\nexport function assetMetadataWrite(assetId: string, patch: AssetMetadataPatch): AssetWriteInput {\n const metaPatch = patch.metaPatch\n const localizedMetaPatch = patch.localizedMetaPatch\n return {\n id: assetId,\n ...(patch.title !== undefined ? { title: textWrite(patch.title) } : {}),\n ...(patch.focalPoint !== undefined ? { focalPoint: patch.focalPoint } : {}),\n ...(metaPatch !== undefined && Object.keys(metaPatch).length > 0 ? { metaPatch } : {}),\n ...(localizedMetaPatch !== undefined && Object.keys(localizedMetaPatch).length > 0 ? { localizedMetaPatch } : {}),\n }\n}\n\nexport type UpdateAssetMetadataResult =\n | { ok: true; asset: AssetRecord }\n | { ok: false; message: string }\n\n/**\n * Persist a metadata edit via the repository (the operations facade in the\n * studio). No read first: the write carries only the change, and the adapter\n * resolves it against the stored record inside its own transaction (EC-577). An\n * asset that no longer exists is refused there (a metadata write cannot create\n * one). Failures come back as a structured result — never a throw the UI has to\n * guess about.\n */\nexport async function updateAssetMetadata(\n deps: { assets: AssetRepository },\n projectId: string,\n assetId: string,\n patch: AssetMetadataPatch,\n): Promise<UpdateAssetMetadataResult> {\n try {\n const asset = await deps.assets.upsertAsset(projectId, assetMetadataWrite(assetId, patch))\n return { ok: true, asset }\n } catch (cause) {\n return { ok: false, message: cause instanceof Error ? cause.message : String(cause) }\n }\n}\n"]}
1
+ {"version":3,"file":"asset-metadata.js","sourceRoot":"","sources":["../../../src/components/assets/asset-metadata.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CollectionDef, FieldDef } from '@elytracms/core/cms-core'\nimport { assetMetaIsLocalized, assetMetaValueAt, liftLegacyAlt, metaPatchBetween } from '@elytracms/core/persistence'\nimport type {\n AssetMetaDrift,\n AssetReadLocale,\n AssetRecord,\n AssetRepository,\n AssetWriteInput,\n} from '@elytracms/core/persistence'\n\n/**\n * Asset metadata editing (EC-152). A patch is applied through the\n * {@link AssetRepository} interface — in the studio that is the EC-141\n * operations facade, so every edit runs the `assets.upsert` command\n * (validate → authorize → mutate → event). Pure orchestration; injectable\n * for tests.\n */\n\n/**\n * System columns the upload pipeline owns — never editable metadata, so they're\n * excluded from the asset collection's schema-driven forms: the image field's\n * options dialog (EC-301, `field-control.tsx`) and the assets rail's detail\n * surface (EC-319, `asset-detail.tsx`) share this ONE reserved set so neither\n * drifts from the other. Everything else a project declares on the `kind:'asset'`\n * collection round-trips through the record's open `meta` bag.\n */\nexport const SYSTEM_ASSET_FIELDS: ReadonlySet<string> = new Set([\n 'filename',\n 'url',\n 'contentType',\n 'byteSize',\n 'width',\n 'height',\n 'storageKey',\n 'title',\n])\n\n/**\n * The asset collection's EDITABLE metadata fields — declared fields minus the\n * system columns above and anything explicitly `readOnly` (EC-301/EC-319).\n */\nexport function assetMetaFieldsOf(collection: CollectionDef): FieldDef[] {\n return collection.fields.filter((f) => !SYSTEM_ASSET_FIELDS.has(f.name) && !f.form?.readOnly)\n}\n\n/** Drop `undefined` values so a persisted meta bag stays JSON/Convex-clean. */\nexport function pruneMeta(meta: Record<string, unknown>): Record<string, unknown> {\n return Object.fromEntries(Object.entries(meta).filter(([, v]) => v !== undefined))\n}\n\n/** Human-readable byte size (`1536` → `1.5 KB`). Shared by the asset grid + detail. */\nexport function formatBytes(byteSize: number): string {\n if (byteSize < 1024) return `${byteSize} B`\n if (byteSize < 1024 * 1024) return `${(byteSize / 1024).toFixed(1)} KB`\n return `${(byteSize / (1024 * 1024)).toFixed(1)} MB`\n}\n\n/**\n * An editor's metadata change, in the EC-577 write rule: an ABSENT key leaves the\n * stored value untouched, `null` clears it, a value sets it.\n */\nexport interface AssetMetadataPatch {\n /** New display title: a value sets (trimmed), `null` or a trimmed-empty string clears. */\n title?: string | null\n /**\n * New focal point (EC-248), the normalized 0–1 hotspot threaded to delivery's\n * `object-position` (EC-230). `{x,y}` sets it, `null` clears it (\"Reset to\n * centre\").\n */\n focalPoint?: { x: number; y: number } | null\n /**\n * A per-key change to the schema-driven metadata bag (EC-301 → EC-634): alt,\n * copyright, credit, … keyed by the asset collection's `fields`. A key with a value\n * sets it, `null` removes it, a key the patch does not name keeps its STORED value —\n * applied to the bag as stored, inside the write (core `resolveAssetWrite`), so an\n * editor never writes back a stale copy of a key another session changed. Build it\n * with {@link assetMetaDraftPatch} from the draft the editor opened and the one it holds.\n * `meta.alt` is the asset's ONE shared alt (EC-587) — there is no separate alt key. There\n * is no whole-bag form here: every studio editor of an asset's metadata patches per key.\n */\n metaPatch?: Record<string, unknown>\n /**\n * EC-632 — the same per-key change to the per-locale buckets of the asset collection's\n * `localized: true` fields: `{ [locale]: { [field]: value | null } }`, applied per locale\n * and key to the stored buckets inside the write.\n */\n localizedMetaPatch?: Record<string, Record<string, unknown>>\n}\n\n/**\n * An asset's metadata as an editor holds it (EC-632) — the DOCUMENT shape, so editing it\n * is the mechanism documents use: the shared bag (a document's `values`) and the\n * per-locale buckets of the `localized: true` fields (its `localized`). The form reads and\n * writes one field in one locale ({@link assetMetaFieldValue} / {@link setAssetMetaField});\n * the save is a structural diff of the whole draft ({@link assetMetaDraftPatch}), so it\n * needs no config and never writes a key or a locale the editor did not change.\n */\nexport interface AssetMetaDraft {\n readonly meta: Readonly<Record<string, unknown>>\n readonly localizedMeta: Readonly<Record<string, Readonly<Record<string, unknown>>>>\n}\n\n/**\n * The fields of the asset collection an editor edits by — only the name matters here.\n * EC-659: whether a field is edited per locale is the read locale's LENS (`at.lens`, the\n * project's asset collection as declared now — the one delivery reads by), never the\n * form's own copy of the field: a mount's rail override cannot make the editor write a\n * translation delivery would not read.\n */\ntype MetaField = Pick<FieldDef, 'name'>\n\n/**\n * The draft an editor opens (EC-587 + EC-632): the stored bags, with a legacy `alt` column\n * lifted into the shared one (core `liftLegacyAlt`), so a not-yet-migrated asset shows the\n * alt every reader shows — and its first save moves it home.\n */\nexport function assetMetaDraft(record: Pick<AssetRecord, 'alt' | 'meta' | 'localizedMeta'>): AssetMetaDraft {\n const localizedMeta: Record<string, Record<string, unknown>> = {}\n for (const [locale, bucket] of Object.entries(record.localizedMeta ?? {})) localizedMeta[locale] = { ...bucket }\n return { meta: liftLegacyAlt(record)?.meta ?? { ...(record.meta ?? {}) }, localizedMeta }\n}\n\nconst hasOwn = (bag: Readonly<Record<string, unknown>> | undefined, key: string): boolean =>\n bag !== undefined && Object.prototype.hasOwnProperty.call(bag, key)\n\n/**\n * What the form shows for one field (EC-632) — read by THE positional rule delivery's view\n * reads by (core `assetMetaValueAt`, EC-659), with the editor's fallback (`'own'`): a field\n * the lens localizes shows the editor's locale's own value (an untranslated locale shows\n * empty — it falls back to the default locale on delivery); the default locale also shows a\n * value written before the field was localized (the shared bag — delivery's last-resort\n * fallback, so it IS that locale's value). Any other field the shared value — a translation\n * left from a former localization is drift, shown by no reader. Without a locale config,\n * the shared value.\n */\nexport function assetMetaFieldValue(draft: AssetMetaDraft, field: MetaField, at: AssetReadLocale | null): unknown {\n const read = assetMetaValueAt(draft, field.name, at, 'own')\n return read.found ? read.value : undefined\n}\n\n/**\n * Write one field of the draft (EC-632) — `undefined` removes the value. Where it goes is\n * the lens's answer (EC-659, `assetMetaIsLocalized` — the rule every reader reads by):\n * - a field the lens localizes goes into the editor's locale's bucket; in the DEFAULT\n * locale a value written before the field was localized leaves the shared bag — the\n * default bucket is its home now, so a clear there really clears it;\n * - any other field goes into the shared bag, and a value a former localization left in a\n * bucket is removed (one home per key — the stale value is drift, read by nobody; this\n * edit is how an editor clears it);\n * - without a locale config (`at` null) there is no lens: the shared bag, and every\n * bucket left alone — a translation is never removed on a guess.\n */\nexport function setAssetMetaField(\n draft: AssetMetaDraft,\n field: MetaField,\n at: AssetReadLocale | null,\n value: unknown,\n): AssetMetaDraft {\n const name = field.name\n const withValue = (bag: Readonly<Record<string, unknown>> | undefined): Record<string, unknown> => {\n const next = { ...(bag ?? {}) }\n if (value === undefined) delete next[name]\n else next[name] = value\n return next\n }\n const without = (bag: Readonly<Record<string, unknown>>): Record<string, unknown> => {\n const next = { ...bag }\n delete next[name]\n return next\n }\n // No locale config installed yet (no lens): the shared bag, the buckets left untouched.\n if (at === null) return { meta: withValue(draft.meta), localizedMeta: draft.localizedMeta }\n if (assetMetaIsLocalized(at, name)) {\n const bucket = withValue(draft.localizedMeta[at.locale])\n const localizedMeta = { ...draft.localizedMeta, [at.locale]: bucket }\n const meta = at.locale === at.default && hasOwn(draft.meta, name) ? without(draft.meta) : draft.meta\n return { meta, localizedMeta }\n }\n const localizedMeta: Record<string, Readonly<Record<string, unknown>>> = {}\n for (const [locale, bucket] of Object.entries(draft.localizedMeta)) {\n localizedMeta[locale] = hasOwn(bucket, name) ? without(bucket) : bucket\n }\n return { meta: withValue(draft.meta), localizedMeta }\n}\n\n/**\n * The per-key, per-locale patch of a metadata edit (EC-634 × EC-632): what changed between\n * the draft the editor OPENED ({@link assetMetaDraft} at open, or the draft of its last\n * successful save) and the draft it holds — `undefined` values pruned, a removed key\n * `null`, a locale it did not touch absent. Every asset metadata editor (the image options\n * dialog, the asset library's detail, the assets rail) sends this instead of its buffer.\n */\nexport function assetMetaDraftPatch(\n opened: AssetMetaDraft,\n edited: AssetMetaDraft,\n): Pick<AssetMetadataPatch, 'metaPatch' | 'localizedMetaPatch'> {\n const metaPatch = metaPatchBetween(pruneMeta({ ...opened.meta }), pruneMeta({ ...edited.meta }))\n const localizedMetaPatch: Record<string, Record<string, unknown>> = {}\n for (const locale of new Set([...Object.keys(opened.localizedMeta), ...Object.keys(edited.localizedMeta)])) {\n const patch = metaPatchBetween(\n pruneMeta({ ...(opened.localizedMeta[locale] ?? {}) }),\n pruneMeta({ ...(edited.localizedMeta[locale] ?? {}) }),\n )\n if (Object.keys(patch).length > 0) localizedMetaPatch[locale] = patch\n }\n return {\n ...(Object.keys(metaPatch).length > 0 ? { metaPatch } : {}),\n ...(Object.keys(localizedMetaPatch).length > 0 ? { localizedMetaPatch } : {}),\n }\n}\n\n/**\n * The asset detail's drift notice, as data (EC-659): the stored translations the lens does\n * not read (core `assetMetaDrift`), grouped by reason, each field listed with the locales\n * holding one — `alt (de, fr)`. Empty when there is none. Pure.\n */\nexport function assetMetaDriftSummary(\n drift: readonly AssetMetaDrift[],\n): Array<{ reason: AssetMetaDrift['reason']; fields: string }> {\n const lines: Array<{ reason: AssetMetaDrift['reason']; fields: string }> = []\n for (const reason of ['not-localized', 'undeclared'] as const) {\n const byField = new Map<string, string[]>()\n for (const entry of drift) {\n if (entry.reason !== reason) continue\n byField.set(entry.field, [...(byField.get(entry.field) ?? []), entry.locale])\n }\n if (byField.size === 0) continue\n const fields = [...byField.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([field, locales]) => `${field} (${locales.join(', ')})`)\n .join(', ')\n lines.push({ reason, fields })\n }\n return lines\n}\n\n/** The form's flat view of a draft in one locale — `{ [field]: value }` for the given fields. */\nexport function assetMetaFormValues(\n draft: AssetMetaDraft,\n fields: readonly MetaField[],\n at: AssetReadLocale | null,\n): Record<string, unknown> {\n return Object.fromEntries(fields.map((field) => [field.name, assetMetaFieldValue(draft, field, at)]))\n}\n\n/** A text column's write: a trimmed value sets, `null`/empty clears. */\nfunction textWrite(value: string | null): string | null {\n const trimmed = value?.trim() ?? ''\n return trimmed.length > 0 ? trimmed : null\n}\n\n/**\n * The asset write for a metadata edit (EC-577): ONLY the keys the editor changed,\n * with `null` for a clear. It deliberately restates nothing else — not the\n * upload-owned columns, and never `thumb`/`lqip`: a stale copy of those would\n * overwrite a preview backfill that finished while the dialog was open, and a\n * clear expressed as an omitted key is exactly what never persisted on Convex.\n *\n * EC-634: `meta` travels as the per-key `metaPatch` — never the whole bag, which would\n * carry a stale copy of every key the editor did not change. An empty patch is not sent.\n * The legacy `alt` column (EC-587) is lifted into the stored bag and cleared by the write\n * rule itself, in the same transaction, whenever a patch is applied — so an alt the editor\n * did not touch survives, and one the editor cleared stays cleared (ASSETS D21).\n */\nexport function assetMetadataWrite(assetId: string, patch: AssetMetadataPatch): AssetWriteInput {\n const metaPatch = patch.metaPatch\n const localizedMetaPatch = patch.localizedMetaPatch\n return {\n id: assetId,\n ...(patch.title !== undefined ? { title: textWrite(patch.title) } : {}),\n ...(patch.focalPoint !== undefined ? { focalPoint: patch.focalPoint } : {}),\n ...(metaPatch !== undefined && Object.keys(metaPatch).length > 0 ? { metaPatch } : {}),\n ...(localizedMetaPatch !== undefined && Object.keys(localizedMetaPatch).length > 0 ? { localizedMetaPatch } : {}),\n }\n}\n\nexport type UpdateAssetMetadataResult =\n | { ok: true; asset: AssetRecord }\n | { ok: false; message: string }\n\n/**\n * Persist a metadata edit via the repository (the operations facade in the\n * studio). No read first: the write carries only the change, and the adapter\n * resolves it against the stored record inside its own transaction (EC-577). An\n * asset that no longer exists is refused there (a metadata write cannot create\n * one). Failures come back as a structured result — never a throw the UI has to\n * guess about.\n */\nexport async function updateAssetMetadata(\n deps: { assets: AssetRepository },\n projectId: string,\n assetId: string,\n patch: AssetMetadataPatch,\n): Promise<UpdateAssetMetadataResult> {\n try {\n const asset = await deps.assets.upsertAsset(projectId, assetMetadataWrite(assetId, patch))\n return { ok: true, asset }\n } catch (cause) {\n return { ok: false, message: cause instanceof Error ? cause.message : String(cause) }\n }\n}\n"]}
@@ -1,7 +1,11 @@
1
- import type { AssetRecord } from '@elytracms/core/persistence';
1
+ import type { AssetReadLocale, AssetRecord } from '@elytracms/core/persistence';
2
2
  /**
3
3
  * Client-side asset search (EC-152): case-insensitive substring match over
4
4
  * filename, alt text, and title. Pure and deterministic — input order is
5
5
  * preserved; an empty/whitespace query matches everything.
6
+ *
7
+ * EC-659: the alts searched are the ones the readers show — through `at`'s lens (the
8
+ * project's asset collection as declared now), so a translation the schema no longer
9
+ * reads (drift) does not make an asset match.
6
10
  */
7
- export declare function filterAssets(assets: readonly AssetRecord[], query: string): AssetRecord[];
11
+ export declare function filterAssets(assets: readonly AssetRecord[], query: string, at: AssetReadLocale | null): AssetRecord[];
@@ -3,19 +3,29 @@ import { assetAlt } from '@elytracms/core/persistence';
3
3
  * Client-side asset search (EC-152): case-insensitive substring match over
4
4
  * filename, alt text, and title. Pure and deterministic — input order is
5
5
  * preserved; an empty/whitespace query matches everything.
6
+ *
7
+ * EC-659: the alts searched are the ones the readers show — through `at`'s lens (the
8
+ * project's asset collection as declared now), so a translation the schema no longer
9
+ * reads (drift) does not make an asset match.
6
10
  */
7
- export function filterAssets(assets, query) {
11
+ export function filterAssets(assets, query, at) {
8
12
  const q = query.trim().toLowerCase();
9
13
  if (q.length === 0)
10
14
  return [...assets];
11
15
  return assets.filter((asset) =>
12
16
  // EC-587 (ASSETS D20): the asset's alt through THE reader (`meta.alt`, its home) —
13
17
  // EC-632: in every locale it is translated into, so a search finds it from any tab.
14
- [asset.filename, ...searchableAlts(asset), asset.title].some((value) => value != null && value.toLowerCase().includes(q)));
18
+ [asset.filename, ...searchableAlts(asset, at), asset.title].some((value) => value != null && value.toLowerCase().includes(q)));
15
19
  }
16
- /** An asset's alt in the shared bag and in each locale it holds a bucket for (EC-632), through THE reader. */
17
- function searchableAlts(asset) {
20
+ /**
21
+ * An asset's alt in the shared bag and in each locale it holds a bucket for (EC-632),
22
+ * through THE reader — each locale read on its own (its bucket, else the shared value),
23
+ * with `at`'s lens deciding whether a bucket is read at all (EC-659).
24
+ */
25
+ function searchableAlts(asset, at) {
26
+ if (at === null)
27
+ return [assetAlt(asset, null)];
18
28
  const locales = Object.keys(asset.localizedMeta ?? {});
19
- return [assetAlt(asset, null), ...locales.map((locale) => assetAlt(asset, { locale, default: locale }))];
29
+ return [assetAlt(asset, null), ...locales.map((locale) => assetAlt(asset, { ...at, locale, default: locale }))];
20
30
  }
21
31
  //# sourceMappingURL=asset-search.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"asset-search.js","sourceRoot":"","sources":["../../../src/components/assets/asset-search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAA;AAGtD;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,MAA8B,EAAE,KAAa;IACxE,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;IACpC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,GAAG,MAAM,CAAC,CAAA;IACtC,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;IAC7B,mFAAmF;IACnF,oFAAoF;IACpF,CAAC,KAAK,CAAC,QAAQ,EAAE,GAAG,cAAc,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAC1D,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,IAAI,IAAI,IAAI,KAAK,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,CAC5D,CACF,CAAA;AACH,CAAC;AAED,8GAA8G;AAC9G,SAAS,cAAc,CAAC,KAAkB;IACxC,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa,IAAI,EAAE,CAAC,CAAA;IACtD,OAAO,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,CAAA;AAC1G,CAAC","sourcesContent":["import { assetAlt } from '@elytracms/core/persistence'\nimport type { AssetRecord } from '@elytracms/core/persistence'\n\n/**\n * Client-side asset search (EC-152): case-insensitive substring match over\n * filename, alt text, and title. Pure and deterministic — input order is\n * preserved; an empty/whitespace query matches everything.\n */\nexport function filterAssets(assets: readonly AssetRecord[], query: string): AssetRecord[] {\n const q = query.trim().toLowerCase()\n if (q.length === 0) return [...assets]\n return assets.filter((asset) =>\n // EC-587 (ASSETS D20): the asset's alt through THE reader (`meta.alt`, its home) —\n // EC-632: in every locale it is translated into, so a search finds it from any tab.\n [asset.filename, ...searchableAlts(asset), asset.title].some(\n (value) => value != null && value.toLowerCase().includes(q),\n ),\n )\n}\n\n/** An asset's alt in the shared bag and in each locale it holds a bucket for (EC-632), through THE reader. */\nfunction searchableAlts(asset: AssetRecord): (string | null)[] {\n const locales = Object.keys(asset.localizedMeta ?? {})\n return [assetAlt(asset, null), ...locales.map((locale) => assetAlt(asset, { locale, default: locale }))]\n}\n"]}
1
+ {"version":3,"file":"asset-search.js","sourceRoot":"","sources":["../../../src/components/assets/asset-search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAA;AAGtD;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,MAA8B,EAAE,KAAa,EAAE,EAA0B;IACpG,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;IACpC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,GAAG,MAAM,CAAC,CAAA;IACtC,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;IAC7B,mFAAmF;IACnF,oFAAoF;IACpF,CAAC,KAAK,CAAC,QAAQ,EAAE,GAAG,cAAc,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAC9D,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,IAAI,IAAI,IAAI,KAAK,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,CAC5D,CACF,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,SAAS,cAAc,CAAC,KAAkB,EAAE,EAA0B;IACpE,IAAI,EAAE,KAAK,IAAI;QAAE,OAAO,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAA;IAC/C,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa,IAAI,EAAE,CAAC,CAAA;IACtD,OAAO,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,EAAE,EAAE,GAAG,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,CAAA;AACjH,CAAC","sourcesContent":["import { assetAlt } from '@elytracms/core/persistence'\nimport type { AssetReadLocale, AssetRecord } from '@elytracms/core/persistence'\n\n/**\n * Client-side asset search (EC-152): case-insensitive substring match over\n * filename, alt text, and title. Pure and deterministic — input order is\n * preserved; an empty/whitespace query matches everything.\n *\n * EC-659: the alts searched are the ones the readers show — through `at`'s lens (the\n * project's asset collection as declared now), so a translation the schema no longer\n * reads (drift) does not make an asset match.\n */\nexport function filterAssets(assets: readonly AssetRecord[], query: string, at: AssetReadLocale | null): AssetRecord[] {\n const q = query.trim().toLowerCase()\n if (q.length === 0) return [...assets]\n return assets.filter((asset) =>\n // EC-587 (ASSETS D20): the asset's alt through THE reader (`meta.alt`, its home) —\n // EC-632: in every locale it is translated into, so a search finds it from any tab.\n [asset.filename, ...searchableAlts(asset, at), asset.title].some(\n (value) => value != null && value.toLowerCase().includes(q),\n ),\n )\n}\n\n/**\n * An asset's alt in the shared bag and in each locale it holds a bucket for (EC-632),\n * through THE reader — each locale read on its own (its bucket, else the shared value),\n * with `at`'s lens deciding whether a bucket is read at all (EC-659).\n */\nfunction searchableAlts(asset: AssetRecord, at: AssetReadLocale | null): (string | null)[] {\n if (at === null) return [assetAlt(asset, null)]\n const locales = Object.keys(asset.localizedMeta ?? {})\n return [assetAlt(asset, null), ...locales.map((locale) => assetAlt(asset, { ...at, locale, default: locale }))]\n}\n"]}
@@ -37,7 +37,9 @@ export declare const ASSET_COLLECTION_CAPABILITIES: CollectionViewCapabilities;
37
37
  * EC-632: the values are the metadata's view in `at` (the studio's locale) — a
38
38
  * `localized: true` field shows that locale's value, with delivery's fallback, held in
39
39
  * the lens's one locale bucket (`localized[at.locale]`, the rail's locale), so the table
40
- * reads it as that locale's value rather than as a fallback.
40
+ * reads it as that locale's value rather than as a fallback. EC-659: "localized" is
41
+ * `at`'s lens (the project's asset collection as declared now — what delivery reads by),
42
+ * so the rail never shows a translation delivery does not serve.
41
43
  */
42
44
  export declare function assetToDocument(collection: CollectionDef, record: AssetRecord, at: AssetReadLocale | null): CmsDocument;
43
45
  /**
@@ -51,8 +53,8 @@ export declare function documentToAssetMeta(doc: CmsDocument): Record<string, un
51
53
  /**
52
54
  * The rail save's metadata write (EC-634 × EC-632): only the keys whose value differs
53
55
  * between the document as the editor OPENED it (`base`, forwarded by the edit shell —
54
- * EC-616) and the one it saves, each written where its field lives — a `localized: true`
55
- * field into `at`'s bucket, any other into the shared bag (`setAssetMetaField`, the
56
+ * EC-616) and the one it saves, each written where its field lives — a field `at`'s lens
57
+ * localizes into `at`'s bucket, any other into the shared bag (`setAssetMetaField`, the
56
58
  * editors' one rule) — as the per-key, per-locale patch against the record as stored. A
57
59
  * key this editor did not change is never written. Without a base every key of the saved
58
60
  * bag is set and none is removed.
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useMemo } from 'react';
2
2
  import { createCollectionRegistry, semanticValueEqual, validateDocument } from '@elytracms/core/cms-core';
3
- import { assetAlt, assetMetaView } from '@elytracms/core/persistence';
3
+ import { assetAlt, assetMetaIsLocalized, assetMetaView } from '@elytracms/core/persistence';
4
4
  import { studioPath } from '@elytracms/studio/router';
5
5
  import { useAssets } from './assets-store.js';
6
6
  import { SYSTEM_ASSET_FIELDS, assetMetaDraft, assetMetaDraftPatch, pruneMeta, setAssetMetaField } from './asset-metadata.js';
@@ -87,7 +87,9 @@ function systemFieldValue(record, name) {
87
87
  * EC-632: the values are the metadata's view in `at` (the studio's locale) — a
88
88
  * `localized: true` field shows that locale's value, with delivery's fallback, held in
89
89
  * the lens's one locale bucket (`localized[at.locale]`, the rail's locale), so the table
90
- * reads it as that locale's value rather than as a fallback.
90
+ * reads it as that locale's value rather than as a fallback. EC-659: "localized" is
91
+ * `at`'s lens (the project's asset collection as declared now — what delivery reads by),
92
+ * so the rail never shows a translation delivery does not serve.
91
93
  */
92
94
  export function assetToDocument(collection, record, at) {
93
95
  const values = assetMetaView(record, at);
@@ -107,7 +109,7 @@ export function assetToDocument(collection, record, at) {
107
109
  return { collection: collection.id, id: record.id, values };
108
110
  const bucket = {};
109
111
  for (const field of collection.fields) {
110
- if (field.localized !== true || !Object.prototype.hasOwnProperty.call(values, field.name))
112
+ if (!assetMetaIsLocalized(at, field.name) || !Object.prototype.hasOwnProperty.call(values, field.name))
111
113
  continue;
112
114
  bucket[field.name] = values[field.name];
113
115
  delete values[field.name];
@@ -131,8 +133,8 @@ export function documentToAssetMeta(doc) {
131
133
  /**
132
134
  * The rail save's metadata write (EC-634 × EC-632): only the keys whose value differs
133
135
  * between the document as the editor OPENED it (`base`, forwarded by the edit shell —
134
- * EC-616) and the one it saves, each written where its field lives — a `localized: true`
135
- * field into `at`'s bucket, any other into the shared bag (`setAssetMetaField`, the
136
+ * EC-616) and the one it saves, each written where its field lives — a field `at`'s lens
137
+ * localizes into `at`'s bucket, any other into the shared bag (`setAssetMetaField`, the
136
138
  * editors' one rule) — as the per-key, per-locale patch against the record as stored. A
137
139
  * key this editor did not change is never written. Without a base every key of the saved
138
140
  * bag is set and none is removed.
@@ -1 +1 @@
1
- {"version":3,"file":"assets-collection-view-source.js","sourceRoot":"","sources":["../../../src/components/assets/assets-collection-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo } from 'react'\nimport type { CmsDocument, CollectionDef, DocumentRef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport { createCollectionRegistry, semanticValueEqual, validateDocument } from '@elytracms/core/cms-core'\nimport { assetAlt, assetMetaView } from '@elytracms/core/persistence'\nimport type { AssetReadLocale, AssetRecord, ReferenceTarget } from '@elytracms/core/persistence'\nimport type { OperationError } from '@elytracms/core/operations'\nimport { studioPath } from '@elytracms/studio/router'\nimport type { DeleteAssetResult } from '../../lib/assets'\nimport type { CollectionViewSource, CollectionViewCapabilities } from '../../lib/collection-view/collection-view-source'\nimport { useAssets } from './assets-store'\nimport { SYSTEM_ASSET_FIELDS, assetMetaDraft, assetMetaDraftPatch, pruneMeta, setAssetMetaField } from './asset-metadata'\nimport type { AssetMetadataPatch } from './asset-metadata'\nimport { useAssetReadLocale } from './asset-locale'\nimport { metaBagProjection, normalizeOkResult, useRailViewSource } from '../rail-view-source'\nimport { parseLocaleConfig } from '@elytracms/core/cms-core'\n\n/**\n * The assets rail has no locale switcher of its own (capability `locales: false`): it\n * reads in ONE locale — the studio's (EC-632), a localized field's value in that locale —\n * so its lens is a one-locale document in that locale. Before a project's locales are\n * installed, the engine's neutral `en` (shared metadata only: nothing is localized then).\n */\nconst ASSETS_LOCALE: Locale = 'en'\nfunction railLocaleConfig(at: AssetReadLocale | null): LocaleConfig {\n const locale = at?.locale ?? ASSETS_LOCALE\n return parseLocaleConfig({ default: locale, locales: [locale] })\n}\n\n/**\n * The capability profile of the assets rail (EC-319, EC-433). Hoisted to a\n * module-level constant (no prop/state dependency) so it's both the value the\n * hook returns AND independently unit-testable.\n *\n * `create` is off — a new asset comes from an upload, never a blank draft.\n * `remove` + `bulk` are ON since EC-433: the list's row/bulk delete used to be\n * off deliberately because the generic `ReferenceConfirmDialog` enumerated\n * inbound references as `{ type: 'document' }` targets — the wrong index shape\n * for an asset — which would have shown \"not referenced\" for an asset that IS\n * referenced. That pre-flight now queries the shape THIS source declares via\n * {@link assetReferenceTarget} (`{ type: 'asset' }`, the same lookup the\n * detail surface runs), and the confirmed delete dispatches the SAME checked\n * store delete the detail surface uses (`deleteAsset`: original + EC-400 thumb\n * blobs, then the record — EC-420 lqips ride on the record). Nothing else\n * applies to assets: no publish/versions/locales/inline-edit/live-URL/\n * hierarchy/import-export.\n */\nexport const ASSET_COLLECTION_CAPABILITIES: CollectionViewCapabilities = {\n publish: false,\n versions: false,\n locales: false,\n inlineEdit: false,\n create: false,\n duplicate: false, // EC-442: blob duplication is a different feature, out of scope\n remove: true,\n bulk: true,\n liveUrl: false,\n hierarchy: false,\n importExport: false,\n adminOnly: false,\n}\n\n/** Read one of the record's fixed, upload-managed columns by field name. */\nfunction systemFieldValue(record: AssetRecord, name: string): unknown {\n switch (name) {\n case 'filename':\n return record.filename\n case 'url':\n return record.url\n case 'contentType':\n return record.contentType\n case 'byteSize':\n return record.byteSize\n case 'width':\n return record.width\n case 'height':\n return record.height\n case 'storageKey':\n return record.storageKey\n case 'title':\n return record.title\n default:\n return undefined\n }\n}\n\n/**\n * Project a stored asset record into the lens's `values` shape (EC-319) — the\n * value-keyed projection the redirects/users rails use (EC-577, RESIDUE-3d): EVERY\n * key of the record's open `meta` bag (EC-301) rides in `values`, declared by the\n * schema or not, so a save hands the bag back whole (D22). Declared fields then\n * take their canonical reading on top — a column never loses to a same-named meta\n * key: a system column (`filename`, `url`, dimensions, …) reads straight off the\n * record, and `alt` reads through THE reader (`assetAlt`, EC-587 — `meta.alt`, with a\n * not-yet-migrated legacy column lifted in), exactly what delivery and the image\n * field's options dialog show.\n *\n * EC-632: the values are the metadata's view in `at` (the studio's locale) — a\n * `localized: true` field shows that locale's value, with delivery's fallback, held in\n * the lens's one locale bucket (`localized[at.locale]`, the rail's locale), so the table\n * reads it as that locale's value rather than as a fallback.\n */\nexport function assetToDocument(collection: CollectionDef, record: AssetRecord, at: AssetReadLocale | null): CmsDocument {\n const values: Record<string, unknown> = assetMetaView(record, at)\n for (const field of collection.fields) {\n if (field.name === 'alt') {\n const alt = assetAlt(record, at)\n if (alt !== null) values.alt = alt\n else delete values.alt\n } else if (SYSTEM_ASSET_FIELDS.has(field.name)) {\n values[field.name] = systemFieldValue(record, field.name)\n }\n }\n if (at === null) return { collection: collection.id, id: record.id, values }\n const bucket: Record<string, unknown> = {}\n for (const field of collection.fields) {\n if (field.localized !== true || !Object.prototype.hasOwnProperty.call(values, field.name)) continue\n bucket[field.name] = values[field.name]\n delete values[field.name]\n }\n return Object.keys(bucket).length > 0\n ? { collection: collection.id, id: record.id, values, localized: { [at.locale]: bucket } }\n : { collection: collection.id, id: record.id, values }\n}\n\n/**\n * The inverse of {@link assetToDocument}: the meta bag a lens document holds — every\n * value outside the system set (EC-301/EC-311), declared or not, through the ONE\n * value-keyed projection the users/redirects rails share (EC-577, RESIDUE-3d). A key\n * the lens never declared is in it (the schema-driven projection this replaced dropped\n * them, D22). System columns are upload-managed and never written here.\n */\nexport function documentToAssetMeta(doc: CmsDocument): Record<string, unknown> {\n // EC-632: the lens's one locale bucket (a localized field's value) reads beside `values`.\n const localized = Object.values(doc.localized ?? {}).reduce<Record<string, unknown>>((all, bag) => ({ ...all, ...bag }), {})\n return pruneMeta(metaBagProjection({ ...(doc.values ?? {}), ...localized }, SYSTEM_ASSET_FIELDS))\n}\n\n/**\n * The rail save's metadata write (EC-634 × EC-632): only the keys whose value differs\n * between the document as the editor OPENED it (`base`, forwarded by the edit shell —\n * EC-616) and the one it saves, each written where its field lives — a `localized: true`\n * field into `at`'s bucket, any other into the shared bag (`setAssetMetaField`, the\n * editors' one rule) — as the per-key, per-locale patch against the record as stored. A\n * key this editor did not change is never written. Without a base every key of the saved\n * bag is set and none is removed.\n */\nexport function assetRailMetaPatch(\n collection: CollectionDef,\n record: AssetRecord,\n at: AssetReadLocale | null,\n doc: CmsDocument,\n base?: CmsDocument,\n): Pick<AssetMetadataPatch, 'metaPatch' | 'localizedMetaPatch'> {\n const before = base ? documentToAssetMeta(base) : {}\n const after = documentToAssetMeta(doc)\n const opened = assetMetaDraft(record)\n let edited = opened\n for (const name of new Set([...Object.keys(before), ...Object.keys(after)])) {\n if (semanticValueEqual(before[name], after[name])) continue\n const field = collection.fields.find((f) => f.name === name) ?? { name }\n edited = setAssetMetaField(edited, field, at, after[name])\n }\n return assetMetaDraftPatch(opened, edited)\n}\n\n/** `create` when `capabilities.create === false` — never reached from the UI\n * (list/detail never call it); an explicit throw rather than a silent no-op. */\nfunction createUnsupported(): DocumentRef {\n throw new Error('assets are created by upload, not the assets rail')\n}\n\n/** Assets' `remove()` returns an `outcome`-discriminated {@link DeleteAssetResult}\n * (not the `{ ok }` shape users/redirects use, EC-327). `'error'` surfaces as-is;\n * `'blocked'` (the unconfirmed usage gate fired — nothing was removed) surfaces\n * as an explicit failure too (EC-433): a delete that left the asset in place\n * must NEVER look like success. It is unreachable from this rail's UI — every\n * delete path confirms through the reference dialog and dispatches with\n * `confirmInUse` — but stays honest for any direct caller. `'not-found'` maps\n * to success (idempotent delete: the desired end state — gone — holds). */\nexport function normalizeAssetRemoveResult(result: DeleteAssetResult): OperationError | null {\n if (result.outcome === 'error') return { code: 'unknown', message: result.message }\n if (result.outcome === 'blocked') {\n return {\n code: 'unknown',\n message: `Asset is referenced in ${result.usageCount} ${result.usageCount === 1 ? 'place' : 'places'} and was not deleted.`,\n }\n }\n return null\n}\n\n/**\n * How an asset row is addressed in the EC-155 reference index (EC-433):\n * `{ type: 'asset' }`, never the document shape — so the delete confirmation's\n * inbound pre-flight counts the SAME references `loadInboundAssetReferenceViews`\n * (the detail surface's check) does.\n */\nexport function assetReferenceTarget(ref: DocumentRef): ReferenceTarget {\n return { type: 'asset', id: ref.id }\n}\n\n/**\n * Assemble a {@link CollectionViewSource} for the **assets rail** (EC-319) — the\n * portable list surface over the existing asset library store instead of a\n * bespoke listing. `schema` is the DEPLOYMENT-resolved `kind:'asset'` collection\n * (EC-311: a project's own declared fields, or a mount's `nav.assets({ fields })`\n * override merged over it — see `resolveAssetsSchema` in `app-views/default-modules.tsx`),\n * never a hardcoded engine default — so a project's `copyright`/`credit`/… fields\n * (and any EC-311 override) drive the list's columns/filters exactly as they\n * drive the image field's options dialog.\n *\n * Must be mounted under an `<AssetsProvider>` (for {@link useAssets}) — already\n * ambient at the project level (EC-195), so callers rarely mount their own.\n *\n * The shared scaffold (`useRailViewSource`, EC-327) owns `viewState`/`lastError`\n * plumbing and the assembled source; this hook supplies only the asset-store\n * reads/writes, the capability profile, and the projections above.\n */\nexport function useAssetsCollectionViewSource(\n projectId: string,\n schema: CollectionDef,\n): CollectionViewSource {\n const store = useAssets()\n const registry = useMemo(() => createCollectionRegistry([schema]), [schema])\n // EC-632: the rail reads (and a save writes) a localized field in the studio's locale.\n const at = useAssetReadLocale()\n\n const documents = useCallback(\n () => store.assets.map((record) => assetToDocument(schema, record, at)),\n [store.assets, schema, at],\n )\n const get = useCallback(\n (ref: DocumentRef): CmsDocument | undefined => {\n const record = store.assets.find((a) => a.id === ref.id)\n return record ? assetToDocument(schema, record, at) : undefined\n },\n [store.assets, schema, at],\n )\n const titleOf = useCallback(\n (ref: DocumentRef): string => {\n const record = store.assets.find((a) => a.id === ref.id)\n return record?.title ?? record?.filename ?? ref.id\n },\n [store.assets],\n )\n const localeConfig = useMemo(() => railLocaleConfig(at), [at])\n const validate = useCallback(\n (doc: CmsDocument) => validateDocument(registry, doc, { localeConfig }),\n [registry, localeConfig],\n )\n const save = useCallback(\n async (doc: CmsDocument, base?: CmsDocument) => {\n const record = store.assets.find((a) => a.id === doc.id)\n if (!record) return { ok: false as const, message: `No asset “${doc.id}” is loaded.` }\n return store.updateMetadata(doc.id, assetRailMetaPatch(schema, record, at, doc, base))\n },\n [store, schema, at],\n )\n // The unconfirmed delete (the store's usage gate still applies — a referenced\n // asset comes back `blocked`, surfaced via `lastError`). The UI's delete paths\n // never land here: row/bulk deletes confirm through the reference dialog and\n // dispatch `bulkRemove` below.\n const remove = useCallback((ref: DocumentRef) => store.remove(ref.id), [store])\n // The CONFIRMED delete the bulk/row path dispatches (EC-433): the reference\n // dialog has already enumerated inbound usages (asset-shaped, see\n // `assetReferenceTarget`), so this passes `confirmInUse` exactly like the\n // detail surface's confirmed delete — same blob + thumb + cache handling.\n const bulkRemove = useCallback(\n (ref: DocumentRef) => store.remove(ref.id, { confirmInUse: true }),\n [store],\n )\n\n return useRailViewSource({\n projectId,\n schema,\n capabilities: ASSET_COLLECTION_CAPABILITIES,\n localeConfig,\n documents,\n get,\n titleOf,\n // No `referenceCandidates`/`collections`/`isFieldReadOnly` override: assets carry\n // no relation/asset fields of their own (v1) — nothing cross-collection to\n // resolve — and read-only follows the factory's default `form.readOnly` check.\n detailPath: (id: string) => studioPath.asset(id),\n listPath: studioPath.assets(),\n // EC-433: the delete confirmation must enumerate ASSET-shaped inbound\n // references — the document-shaped default would report \"not referenced\"\n // for a referenced asset.\n referenceTarget: assetReferenceTarget,\n validate,\n create: createUnsupported,\n save,\n remove,\n bulkRemove,\n normalizeSaveResult: normalizeOkResult,\n normalizeRemoveResult: normalizeAssetRemoveResult,\n })\n}\n"]}
1
+ {"version":3,"file":"assets-collection-view-source.js","sourceRoot":"","sources":["../../../src/components/assets/assets-collection-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo } from 'react'\nimport type { CmsDocument, CollectionDef, DocumentRef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport { createCollectionRegistry, semanticValueEqual, validateDocument } from '@elytracms/core/cms-core'\nimport { assetAlt, assetMetaIsLocalized, assetMetaView } from '@elytracms/core/persistence'\nimport type { AssetReadLocale, AssetRecord, ReferenceTarget } from '@elytracms/core/persistence'\nimport type { OperationError } from '@elytracms/core/operations'\nimport { studioPath } from '@elytracms/studio/router'\nimport type { DeleteAssetResult } from '../../lib/assets'\nimport type { CollectionViewSource, CollectionViewCapabilities } from '../../lib/collection-view/collection-view-source'\nimport { useAssets } from './assets-store'\nimport { SYSTEM_ASSET_FIELDS, assetMetaDraft, assetMetaDraftPatch, pruneMeta, setAssetMetaField } from './asset-metadata'\nimport type { AssetMetadataPatch } from './asset-metadata'\nimport { useAssetReadLocale } from './asset-locale'\nimport { metaBagProjection, normalizeOkResult, useRailViewSource } from '../rail-view-source'\nimport { parseLocaleConfig } from '@elytracms/core/cms-core'\n\n/**\n * The assets rail has no locale switcher of its own (capability `locales: false`): it\n * reads in ONE locale — the studio's (EC-632), a localized field's value in that locale —\n * so its lens is a one-locale document in that locale. Before a project's locales are\n * installed, the engine's neutral `en` (shared metadata only: nothing is localized then).\n */\nconst ASSETS_LOCALE: Locale = 'en'\nfunction railLocaleConfig(at: AssetReadLocale | null): LocaleConfig {\n const locale = at?.locale ?? ASSETS_LOCALE\n return parseLocaleConfig({ default: locale, locales: [locale] })\n}\n\n/**\n * The capability profile of the assets rail (EC-319, EC-433). Hoisted to a\n * module-level constant (no prop/state dependency) so it's both the value the\n * hook returns AND independently unit-testable.\n *\n * `create` is off — a new asset comes from an upload, never a blank draft.\n * `remove` + `bulk` are ON since EC-433: the list's row/bulk delete used to be\n * off deliberately because the generic `ReferenceConfirmDialog` enumerated\n * inbound references as `{ type: 'document' }` targets — the wrong index shape\n * for an asset — which would have shown \"not referenced\" for an asset that IS\n * referenced. That pre-flight now queries the shape THIS source declares via\n * {@link assetReferenceTarget} (`{ type: 'asset' }`, the same lookup the\n * detail surface runs), and the confirmed delete dispatches the SAME checked\n * store delete the detail surface uses (`deleteAsset`: original + EC-400 thumb\n * blobs, then the record — EC-420 lqips ride on the record). Nothing else\n * applies to assets: no publish/versions/locales/inline-edit/live-URL/\n * hierarchy/import-export.\n */\nexport const ASSET_COLLECTION_CAPABILITIES: CollectionViewCapabilities = {\n publish: false,\n versions: false,\n locales: false,\n inlineEdit: false,\n create: false,\n duplicate: false, // EC-442: blob duplication is a different feature, out of scope\n remove: true,\n bulk: true,\n liveUrl: false,\n hierarchy: false,\n importExport: false,\n adminOnly: false,\n}\n\n/** Read one of the record's fixed, upload-managed columns by field name. */\nfunction systemFieldValue(record: AssetRecord, name: string): unknown {\n switch (name) {\n case 'filename':\n return record.filename\n case 'url':\n return record.url\n case 'contentType':\n return record.contentType\n case 'byteSize':\n return record.byteSize\n case 'width':\n return record.width\n case 'height':\n return record.height\n case 'storageKey':\n return record.storageKey\n case 'title':\n return record.title\n default:\n return undefined\n }\n}\n\n/**\n * Project a stored asset record into the lens's `values` shape (EC-319) — the\n * value-keyed projection the redirects/users rails use (EC-577, RESIDUE-3d): EVERY\n * key of the record's open `meta` bag (EC-301) rides in `values`, declared by the\n * schema or not, so a save hands the bag back whole (D22). Declared fields then\n * take their canonical reading on top — a column never loses to a same-named meta\n * key: a system column (`filename`, `url`, dimensions, …) reads straight off the\n * record, and `alt` reads through THE reader (`assetAlt`, EC-587 — `meta.alt`, with a\n * not-yet-migrated legacy column lifted in), exactly what delivery and the image\n * field's options dialog show.\n *\n * EC-632: the values are the metadata's view in `at` (the studio's locale) — a\n * `localized: true` field shows that locale's value, with delivery's fallback, held in\n * the lens's one locale bucket (`localized[at.locale]`, the rail's locale), so the table\n * reads it as that locale's value rather than as a fallback. EC-659: \"localized\" is\n * `at`'s lens (the project's asset collection as declared now — what delivery reads by),\n * so the rail never shows a translation delivery does not serve.\n */\nexport function assetToDocument(collection: CollectionDef, record: AssetRecord, at: AssetReadLocale | null): CmsDocument {\n const values: Record<string, unknown> = assetMetaView(record, at)\n for (const field of collection.fields) {\n if (field.name === 'alt') {\n const alt = assetAlt(record, at)\n if (alt !== null) values.alt = alt\n else delete values.alt\n } else if (SYSTEM_ASSET_FIELDS.has(field.name)) {\n values[field.name] = systemFieldValue(record, field.name)\n }\n }\n if (at === null) return { collection: collection.id, id: record.id, values }\n const bucket: Record<string, unknown> = {}\n for (const field of collection.fields) {\n if (!assetMetaIsLocalized(at, field.name) || !Object.prototype.hasOwnProperty.call(values, field.name)) continue\n bucket[field.name] = values[field.name]\n delete values[field.name]\n }\n return Object.keys(bucket).length > 0\n ? { collection: collection.id, id: record.id, values, localized: { [at.locale]: bucket } }\n : { collection: collection.id, id: record.id, values }\n}\n\n/**\n * The inverse of {@link assetToDocument}: the meta bag a lens document holds — every\n * value outside the system set (EC-301/EC-311), declared or not, through the ONE\n * value-keyed projection the users/redirects rails share (EC-577, RESIDUE-3d). A key\n * the lens never declared is in it (the schema-driven projection this replaced dropped\n * them, D22). System columns are upload-managed and never written here.\n */\nexport function documentToAssetMeta(doc: CmsDocument): Record<string, unknown> {\n // EC-632: the lens's one locale bucket (a localized field's value) reads beside `values`.\n const localized = Object.values(doc.localized ?? {}).reduce<Record<string, unknown>>((all, bag) => ({ ...all, ...bag }), {})\n return pruneMeta(metaBagProjection({ ...(doc.values ?? {}), ...localized }, SYSTEM_ASSET_FIELDS))\n}\n\n/**\n * The rail save's metadata write (EC-634 × EC-632): only the keys whose value differs\n * between the document as the editor OPENED it (`base`, forwarded by the edit shell —\n * EC-616) and the one it saves, each written where its field lives — a field `at`'s lens\n * localizes into `at`'s bucket, any other into the shared bag (`setAssetMetaField`, the\n * editors' one rule) — as the per-key, per-locale patch against the record as stored. A\n * key this editor did not change is never written. Without a base every key of the saved\n * bag is set and none is removed.\n */\nexport function assetRailMetaPatch(\n collection: CollectionDef,\n record: AssetRecord,\n at: AssetReadLocale | null,\n doc: CmsDocument,\n base?: CmsDocument,\n): Pick<AssetMetadataPatch, 'metaPatch' | 'localizedMetaPatch'> {\n const before = base ? documentToAssetMeta(base) : {}\n const after = documentToAssetMeta(doc)\n const opened = assetMetaDraft(record)\n let edited = opened\n for (const name of new Set([...Object.keys(before), ...Object.keys(after)])) {\n if (semanticValueEqual(before[name], after[name])) continue\n const field = collection.fields.find((f) => f.name === name) ?? { name }\n edited = setAssetMetaField(edited, field, at, after[name])\n }\n return assetMetaDraftPatch(opened, edited)\n}\n\n/** `create` when `capabilities.create === false` — never reached from the UI\n * (list/detail never call it); an explicit throw rather than a silent no-op. */\nfunction createUnsupported(): DocumentRef {\n throw new Error('assets are created by upload, not the assets rail')\n}\n\n/** Assets' `remove()` returns an `outcome`-discriminated {@link DeleteAssetResult}\n * (not the `{ ok }` shape users/redirects use, EC-327). `'error'` surfaces as-is;\n * `'blocked'` (the unconfirmed usage gate fired — nothing was removed) surfaces\n * as an explicit failure too (EC-433): a delete that left the asset in place\n * must NEVER look like success. It is unreachable from this rail's UI — every\n * delete path confirms through the reference dialog and dispatches with\n * `confirmInUse` — but stays honest for any direct caller. `'not-found'` maps\n * to success (idempotent delete: the desired end state — gone — holds). */\nexport function normalizeAssetRemoveResult(result: DeleteAssetResult): OperationError | null {\n if (result.outcome === 'error') return { code: 'unknown', message: result.message }\n if (result.outcome === 'blocked') {\n return {\n code: 'unknown',\n message: `Asset is referenced in ${result.usageCount} ${result.usageCount === 1 ? 'place' : 'places'} and was not deleted.`,\n }\n }\n return null\n}\n\n/**\n * How an asset row is addressed in the EC-155 reference index (EC-433):\n * `{ type: 'asset' }`, never the document shape — so the delete confirmation's\n * inbound pre-flight counts the SAME references `loadInboundAssetReferenceViews`\n * (the detail surface's check) does.\n */\nexport function assetReferenceTarget(ref: DocumentRef): ReferenceTarget {\n return { type: 'asset', id: ref.id }\n}\n\n/**\n * Assemble a {@link CollectionViewSource} for the **assets rail** (EC-319) — the\n * portable list surface over the existing asset library store instead of a\n * bespoke listing. `schema` is the DEPLOYMENT-resolved `kind:'asset'` collection\n * (EC-311: a project's own declared fields, or a mount's `nav.assets({ fields })`\n * override merged over it — see `resolveAssetsSchema` in `app-views/default-modules.tsx`),\n * never a hardcoded engine default — so a project's `copyright`/`credit`/… fields\n * (and any EC-311 override) drive the list's columns/filters exactly as they\n * drive the image field's options dialog.\n *\n * Must be mounted under an `<AssetsProvider>` (for {@link useAssets}) — already\n * ambient at the project level (EC-195), so callers rarely mount their own.\n *\n * The shared scaffold (`useRailViewSource`, EC-327) owns `viewState`/`lastError`\n * plumbing and the assembled source; this hook supplies only the asset-store\n * reads/writes, the capability profile, and the projections above.\n */\nexport function useAssetsCollectionViewSource(\n projectId: string,\n schema: CollectionDef,\n): CollectionViewSource {\n const store = useAssets()\n const registry = useMemo(() => createCollectionRegistry([schema]), [schema])\n // EC-632: the rail reads (and a save writes) a localized field in the studio's locale.\n const at = useAssetReadLocale()\n\n const documents = useCallback(\n () => store.assets.map((record) => assetToDocument(schema, record, at)),\n [store.assets, schema, at],\n )\n const get = useCallback(\n (ref: DocumentRef): CmsDocument | undefined => {\n const record = store.assets.find((a) => a.id === ref.id)\n return record ? assetToDocument(schema, record, at) : undefined\n },\n [store.assets, schema, at],\n )\n const titleOf = useCallback(\n (ref: DocumentRef): string => {\n const record = store.assets.find((a) => a.id === ref.id)\n return record?.title ?? record?.filename ?? ref.id\n },\n [store.assets],\n )\n const localeConfig = useMemo(() => railLocaleConfig(at), [at])\n const validate = useCallback(\n (doc: CmsDocument) => validateDocument(registry, doc, { localeConfig }),\n [registry, localeConfig],\n )\n const save = useCallback(\n async (doc: CmsDocument, base?: CmsDocument) => {\n const record = store.assets.find((a) => a.id === doc.id)\n if (!record) return { ok: false as const, message: `No asset “${doc.id}” is loaded.` }\n return store.updateMetadata(doc.id, assetRailMetaPatch(schema, record, at, doc, base))\n },\n [store, schema, at],\n )\n // The unconfirmed delete (the store's usage gate still applies — a referenced\n // asset comes back `blocked`, surfaced via `lastError`). The UI's delete paths\n // never land here: row/bulk deletes confirm through the reference dialog and\n // dispatch `bulkRemove` below.\n const remove = useCallback((ref: DocumentRef) => store.remove(ref.id), [store])\n // The CONFIRMED delete the bulk/row path dispatches (EC-433): the reference\n // dialog has already enumerated inbound usages (asset-shaped, see\n // `assetReferenceTarget`), so this passes `confirmInUse` exactly like the\n // detail surface's confirmed delete — same blob + thumb + cache handling.\n const bulkRemove = useCallback(\n (ref: DocumentRef) => store.remove(ref.id, { confirmInUse: true }),\n [store],\n )\n\n return useRailViewSource({\n projectId,\n schema,\n capabilities: ASSET_COLLECTION_CAPABILITIES,\n localeConfig,\n documents,\n get,\n titleOf,\n // No `referenceCandidates`/`collections`/`isFieldReadOnly` override: assets carry\n // no relation/asset fields of their own (v1) — nothing cross-collection to\n // resolve — and read-only follows the factory's default `form.readOnly` check.\n detailPath: (id: string) => studioPath.asset(id),\n listPath: studioPath.assets(),\n // EC-433: the delete confirmation must enumerate ASSET-shaped inbound\n // references — the document-shaped default would report \"not referenced\"\n // for a referenced asset.\n referenceTarget: assetReferenceTarget,\n validate,\n create: createUnsupported,\n save,\n remove,\n bulkRemove,\n normalizeSaveResult: normalizeOkResult,\n normalizeRemoveResult: normalizeAssetRemoveResult,\n })\n}\n"]}
@@ -35,34 +35,53 @@ export interface RebasableState {
35
35
  export declare function rebaseCanvas(state: RebasableState, from: unknown, to: unknown): RebasableState | undefined;
36
36
  /**
37
37
  * A change of the canvas field's value made UNDERNEATH the canvas, as the document draft
38
- * saw it happen: the field's value before and after the session's document moved, and a
39
- * sequence number (one per change, increasing).
38
+ * saw it happen: the field's value before and after the session's document moved, a
39
+ * sequence number (one per change, increasing), and the revision the draft is at after it
40
+ * (EC-647). `from` ≡ `to` when only the revision moved — the field held still while the
41
+ * document advanced (a write of another field landed, the canvas's own write was
42
+ * acknowledged): the canvas's agreed value is then valid at the newer revision too.
40
43
  */
41
44
  export interface CanvasFieldChange {
42
45
  readonly from: unknown;
43
46
  readonly to: unknown;
44
47
  readonly seq: number;
48
+ readonly revision: number | undefined;
45
49
  }
46
- /** What the canvas lifts into the draft: its value and the value its delta is measured from. */
50
+ /**
51
+ * What the canvas lifts into the draft: its value, the value its delta is measured from,
52
+ * and the revision that base value was agreed at — the edit's AUTHORING revision (EC-647:
53
+ * a value and its revision travel together; the draft's revision may already be newer
54
+ * than anything the canvas has seen).
55
+ */
47
56
  export interface CanvasLift {
48
57
  readonly value: unknown;
49
58
  /** The value the store last agreed on with the draft — the canvas writer's delta is `base` → `value`. */
50
59
  readonly base: unknown;
60
+ /** The revision `base` was agreed at (`undefined`: the draft's view had none). */
61
+ readonly revision: number | undefined;
51
62
  }
52
63
  /**
53
64
  * EC-630 — the two-way sync between the canvas store and the document draft (pure; the
54
65
  * canvas's `CompositionSync` is its React shell).
55
66
  *
56
- * It tracks the value the store and the draft last AGREED on (`synced`):
67
+ * It tracks the value the store and the draft last AGREED on (`synced`) and — EC-647 —
68
+ * the revision it was agreed at, as one value:
57
69
  * - {@link CanvasSync.stored} — the store's value changed (an edit, an undo, a rebase's
58
- * adoption): lift it, with `synced` as the base of the canvas writer's delta, unless
59
- * the draft already holds it. Measuring the delta from `synced` — never from the draft's
60
- * current value — is what keeps a colleague's change that reached the draft a moment
61
- * before an edit lifts out of that edit's patches (they would otherwise revert it).
70
+ * adoption): lift it, with `synced` as the base of the canvas writer's delta and its
71
+ * revision as the edit's authoring revision, unless the draft already holds it.
72
+ * Measuring the delta from `synced` — never from the draft's current value — is what
73
+ * keeps a colleague's change that reached the draft a moment before an edit lifts out
74
+ * of that edit's patches (they would otherwise revert it); pairing it with `synced`'s
75
+ * revision — never the draft's — is what makes the store refuse that edit where it
76
+ * overlaps the colleague's change (Codex F12), instead of accepting it at a revision
77
+ * that already holds a change the canvas never saw. The canvas's own lift keeps the
78
+ * revision (its own edit is not a revision of the document).
62
79
  * - {@link CanvasSync.changed} — the draft reports a change made underneath: `synced`
63
- * absorbs it, and the action to dispatch is a `rebase`. Until the store has applied
64
- * every rebase dispatched (its `rebased` count), a store value predates one and is not
65
- * lifted (its edits are lifted from the rebased value right after).
80
+ * absorbs it AND takes the change's revision (a rebase rebases the value and the
81
+ * revision), and the action to dispatch is a `rebase` — none when only the revision
82
+ * moved. Until the store has applied every rebase dispatched (its `rebased` count), a
83
+ * store value predates one and is not lifted (its edits are lifted from the rebased
84
+ * value right after).
66
85
  */
67
86
  export interface CanvasSync {
68
87
  changed(change: CanvasFieldChange): {
@@ -72,6 +91,11 @@ export interface CanvasSync {
72
91
  } | undefined;
73
92
  stored(value: unknown, rebased: number): CanvasLift | undefined;
74
93
  }
75
- export declare function createCanvasSync(initial: unknown, seenSeq?: number): CanvasSync;
94
+ /**
95
+ * The sync of a canvas mounted on `initial` — the draft's value of the field at `revision`
96
+ * (the revision the draft was at when the canvas mounted) — having seen the draft's
97
+ * changes up to `seenSeq`.
98
+ */
99
+ export declare function createCanvasSync(initial: unknown, revision: number | undefined, seenSeq?: number): CanvasSync;
76
100
  /** The selection after a rebase: kept while its node exists, else its nearest surviving ancestor, else the root. */
77
101
  export declare function survivingSelection(before: ComponentNode, after: ComponentNode, selectedId: string | null): string | null;
@@ -130,8 +130,14 @@ export function rebaseCanvas(state, from, to) {
130
130
  hoveredId: state.hoveredId !== null && getNode(next.root, state.hoveredId) ? state.hoveredId : null,
131
131
  };
132
132
  }
133
- export function createCanvasSync(initial, seenSeq = 0) {
133
+ /**
134
+ * The sync of a canvas mounted on `initial` — the draft's value of the field at `revision`
135
+ * (the revision the draft was at when the canvas mounted) — having seen the draft's
136
+ * changes up to `seenSeq`.
137
+ */
138
+ export function createCanvasSync(initial, revision, seenSeq = 0) {
134
139
  let synced = initial;
140
+ let agreedAt = revision;
135
141
  let seen = seenSeq;
136
142
  let dispatched = 0;
137
143
  return {
@@ -139,7 +145,11 @@ export function createCanvasSync(initial, seenSeq = 0) {
139
145
  if (change.seq <= seen)
140
146
  return undefined;
141
147
  seen = change.seq;
142
- synced = absorbIntoValue(synced, externalChange(change.from, change.to))?.value ?? change.to;
148
+ agreedAt = change.revision;
149
+ const delta = externalChange(change.from, change.to);
150
+ if (delta.length === 0)
151
+ return undefined;
152
+ synced = absorbIntoValue(synced, delta)?.value ?? change.to;
143
153
  dispatched += 1;
144
154
  return { type: 'rebase', from: change.from, to: change.to };
145
155
  },
@@ -150,7 +160,7 @@ export function createCanvasSync(initial, seenSeq = 0) {
150
160
  return undefined;
151
161
  const base = synced;
152
162
  synced = value;
153
- return { value, base };
163
+ return { value, base, revision: agreedAt };
154
164
  },
155
165
  };
156
166
  }
@@ -1 +1 @@
1
- {"version":3,"file":"canvas-rebase.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-rebase.ts"],"names":[],"mappings":"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","sourcesContent":["import { applyDocumentPatches, locateNode, semanticValueEqual, valuePathNodeIds } from '@elytracms/core/cms-core'\nimport type { CmsDocument, DocumentPatch } from '@elytracms/core/cms-core'\nimport { DEFAULT_SLOT, getNode, getParentInfo, mergeBlockRoots, splitBlockRoots } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { compositionPatches } from './composition-patches'\nimport { runByKey } from './canvas-runs'\nimport type { PreservedItem } from './canvas-runs'\n\n/**\n * EC-630 — the canvas REBASES onto a change of its field made underneath it (a colleague's\n * patch arriving by subscription, the canonical row a write returned, a resolved\n * conflict) instead of remounting: the change lands, and the editor's undo history and\n * selection survive (EC-595's model — the canvas commits and rebases like every other\n * editor). Pure; the builder reducer's `rebase` action calls {@link rebaseCanvas}.\n *\n * **The external change** is named by the document draft that saw it happen: the field's\n * value before (`from`) and after (`to`) the session's document moved. Its delta is the\n * one the canvas writer itself would emit between the two ({@link externalChange} — node\n * patches keyed by id, or the coarser whole-field write when node verbs cannot say it: a\n * typography change, prose being one position). Measured between two DRAFT values, the\n * delta holds the colleague's change and nothing else — not this editor's own edits (both\n * values hold them), not a canonicalisation of them.\n *\n * **The present absorbs the change** with THE merge (`applyDocumentPatches`, the function\n * both stores run), so an edit of this editor's that the draft has not seen yet survives\n * under it; when the present cannot take it, the incoming value wins. Never re-normalised\n * (receiving a change never writes one back).\n *\n * **Undo never reverts an external change.** The history holds whole-state snapshots, so\n * kept as they were, an undo would restore a pre-change snapshot and the next commit\n * would write patches that silently revert the colleague. Instead every snapshot ABSORBS\n * the same delta: an undo then restores the editor's own earlier state WITH the\n * colleague's change in it.\n * - A `remove` of a node the snapshot does not hold is already absorbed (nothing to\n * resurrect).\n * - Any other patch a snapshot cannot take (its node is not there — the colleague edited\n * a node this editor inserted later, or moved one into it) means restoring it WOULD\n * revert the colleague: the snapshot and every entry beyond it (older undo steps, later\n * redo steps — reachable only through it) are dropped. Conservative: a lost undo step\n * is recoverable, a silently destroyed colleague's edit is not.\n * - Steps the change emptied (an undo that would now change nothing — the colleague wrote\n * the same position later, or rewrote the whole field) are dropped, so every remaining\n * undo does something.\n *\n * **The selection** survives while its node does. When the change removed it, it moves to\n * the nearest surviving ancestor (the rule a local `remove` follows — the inspector stays\n * where the editor was working), else the root. A selected typography RUN stays selected\n * only while the typography is unchanged: runs carry no ids (EC-500 D1), so after a prose\n * change a key no longer names the same run with certainty.\n */\n\n/** The field name the canvas's own deltas are addressed at (internal — never stored, never sent). */\nconst CANVAS_FIELD = 'canvas'\n\n/** One composition state: the synthetic root and the preserved typography (a history entry's shape). */\nexport interface CanvasValueState {\n readonly root: ComponentNode\n readonly preserved: readonly PreservedItem[]\n}\n\n/** The stored v2 sequence a composition state stands for (THE merge order). */\nexport function canvasValueOf(state: CanvasValueState): unknown {\n return mergeBlockRoots(state.root.slots?.[DEFAULT_SLOT] ?? [], state.preserved)\n}\n\n/** A composition state holding `value` under the given synthetic root (its children replaced). */\nfunction stateOfValue(root: ComponentNode, value: unknown): CanvasValueState {\n const split = splitBlockRoots(value)\n return { root: { ...root, slots: { ...root.slots, [DEFAULT_SLOT]: split.roots } }, preserved: split.preserved }\n}\n\n/** The external change between two values of the field (the canvas writer's delta; `[]` when equal). */\nexport function externalChange(from: unknown, to: unknown): DocumentPatch[] {\n return compositionPatches(from, to, CANVAS_FIELD, undefined)\n}\n\n/** Whether a `remove` names a node this value does not hold (already absorbed). */\nfunction removesAbsent(value: unknown, patch: DocumentPatch): boolean {\n if (patch.op !== 'remove') return false\n const ids = valuePathNodeIds(patch.path)\n const id = ids[ids.length - 1]\n return id !== undefined && locateNode(value, id) === undefined\n}\n\n/** A field value with the change absorbed, or `undefined` when it cannot take it. */\nexport function absorbIntoValue(value: unknown, change: readonly DocumentPatch[]): { value: unknown } | undefined {\n const patches = change.filter((patch) => !removesAbsent(value, patch))\n if (patches.length === 0) return { value }\n const document: CmsDocument = { collection: CANVAS_FIELD, id: CANVAS_FIELD, values: { [CANVAS_FIELD]: value } }\n const result = applyDocumentPatches({ document, patches, since: [] })\n if (result.refused.length > 0) return undefined\n return { value: result.document.values[CANVAS_FIELD] }\n}\n\n/** One composition state with the change absorbed, or `undefined` when it cannot take it. */\nexport function absorbChange(state: CanvasValueState, change: readonly DocumentPatch[]): CanvasValueState | undefined {\n const absorbed = absorbIntoValue(canvasValueOf(state), change)\n return absorbed ? stateOfValue(state.root, absorbed.value) : undefined\n}\n\n/**\n * Rebase one direction of the history (`chain` ordered from the present outward — the\n * newest undo step first, or the next redo step first): each entry absorbs the change,\n * the first that cannot ends the chain, and an entry equal to its neighbour toward the\n * present is dropped. Returns the chain in the same order.\n */\nfunction rebaseChain(\n present: CanvasValueState,\n chain: readonly CanvasValueState[],\n change: readonly DocumentPatch[],\n): CanvasValueState[] {\n const out: CanvasValueState[] = []\n let neighbour = canvasValueOf(present)\n for (const entry of chain) {\n const absorbed = absorbChange(entry, change)\n if (!absorbed) break\n const value = canvasValueOf(absorbed)\n if (semanticValueEqual(value, neighbour)) continue\n out.push(absorbed)\n neighbour = value\n }\n return out\n}\n\n/** The editor state a rebase reads and writes (the builder state's composition, history and selection). */\nexport interface RebasableState {\n readonly present: ComponentNode\n readonly preserved: readonly PreservedItem[]\n /** Oldest first. */\n readonly past: readonly CanvasValueState[]\n /** Next redo first. */\n readonly future: readonly CanvasValueState[]\n readonly selectedId: string | null\n readonly selectedRun: number | null\n readonly hoveredId: string | null\n}\n\n/**\n * Rebase the editor onto the field's change `from` → `to` (see the module doc). Returns\n * `undefined` when there is nothing to change.\n */\nexport function rebaseCanvas(state: RebasableState, from: unknown, to: unknown): RebasableState | undefined {\n const change = externalChange(from, to)\n if (change.length === 0) return undefined\n const current: CanvasValueState = { root: state.present, preserved: state.preserved }\n const next = absorbChange(current, change) ?? stateOfValue(state.present, to)\n const past = rebaseChain(next, [...state.past].reverse(), change).reverse()\n const future = rebaseChain(next, state.future, change)\n const typographyKept = semanticValueEqual(\n state.preserved.map((item) => item.node),\n next.preserved.map((item) => item.node),\n )\n const roots = next.root.slots?.[DEFAULT_SLOT]?.length ?? 0\n return {\n present: next.root,\n preserved: next.preserved,\n past,\n future,\n selectedId: survivingSelection(state.present, next.root, state.selectedId),\n selectedRun:\n state.selectedRun !== null && typographyKept && runByKey(roots, next.preserved, state.selectedRun)\n ? state.selectedRun\n : null,\n hoveredId: state.hoveredId !== null && getNode(next.root, state.hoveredId) ? state.hoveredId : null,\n }\n}\n\n/**\n * A change of the canvas field's value made UNDERNEATH the canvas, as the document draft\n * saw it happen: the field's value before and after the session's document moved, and a\n * sequence number (one per change, increasing).\n */\nexport interface CanvasFieldChange {\n readonly from: unknown\n readonly to: unknown\n readonly seq: number\n}\n\n/** What the canvas lifts into the draft: its value and the value its delta is measured from. */\nexport interface CanvasLift {\n readonly value: unknown\n /** The value the store last agreed on with the draft — the canvas writer's delta is `base` → `value`. */\n readonly base: unknown\n}\n\n/**\n * EC-630 — the two-way sync between the canvas store and the document draft (pure; the\n * canvas's `CompositionSync` is its React shell).\n *\n * It tracks the value the store and the draft last AGREED on (`synced`):\n * - {@link CanvasSync.stored} — the store's value changed (an edit, an undo, a rebase's\n * adoption): lift it, with `synced` as the base of the canvas writer's delta, unless\n * the draft already holds it. Measuring the delta from `synced` — never from the draft's\n * current value — is what keeps a colleague's change that reached the draft a moment\n * before an edit lifts out of that edit's patches (they would otherwise revert it).\n * - {@link CanvasSync.changed} — the draft reports a change made underneath: `synced`\n * absorbs it, and the action to dispatch is a `rebase`. Until the store has applied\n * every rebase dispatched (its `rebased` count), a store value predates one and is not\n * lifted (its edits are lifted from the rebased value right after).\n */\nexport interface CanvasSync {\n changed(change: CanvasFieldChange): { type: 'rebase'; from: unknown; to: unknown } | undefined\n stored(value: unknown, rebased: number): CanvasLift | undefined\n}\n\nexport function createCanvasSync(initial: unknown, seenSeq = 0): CanvasSync {\n let synced = initial\n let seen = seenSeq\n let dispatched = 0\n return {\n changed(change) {\n if (change.seq <= seen) return undefined\n seen = change.seq\n synced = absorbIntoValue(synced, externalChange(change.from, change.to))?.value ?? change.to\n dispatched += 1\n return { type: 'rebase', from: change.from, to: change.to }\n },\n stored(value, rebased) {\n if (rebased < dispatched) return undefined\n if (semanticValueEqual(value, synced)) return undefined\n const base = synced\n synced = value\n return { value, base }\n },\n }\n}\n\n/** The selection after a rebase: kept while its node exists, else its nearest surviving ancestor, else the root. */\nexport function survivingSelection(before: ComponentNode, after: ComponentNode, selectedId: string | null): string | null {\n if (selectedId === null) return null\n let id: string | null = selectedId\n while (id !== null) {\n if (getNode(after, id)) return id\n id = getParentInfo(before, id)?.parent.id ?? null\n }\n return after.id\n}\n"]}
1
+ {"version":3,"file":"canvas-rebase.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-rebase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,UAAU,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAEjH,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAA;AAEtH,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAC1D,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAGxC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,qGAAqG;AACrG,MAAM,YAAY,GAAG,QAAQ,CAAA;AAQ7B,+EAA+E;AAC/E,MAAM,UAAU,aAAa,CAAC,KAAuB;IACnD,OAAO,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,EAAE,KAAK,CAAC,SAAS,CAAC,CAAA;AACjF,CAAC;AAED,kGAAkG;AAClG,SAAS,YAAY,CAAC,IAAmB,EAAE,KAAc;IACvD,MAAM,KAAK,GAAG,eAAe,CAAC,KAAK,CAAC,CAAA;IACpC,OAAO,EAAE,IAAI,EAAE,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,YAAY,CAAC,EAAE,KAAK,CAAC,KAAK,EAAE,EAAE,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAA;AACjH,CAAC;AAED,wGAAwG;AACxG,MAAM,UAAU,cAAc,CAAC,IAAa,EAAE,EAAW;IACvD,OAAO,kBAAkB,CAAC,IAAI,EAAE,EAAE,EAAE,YAAY,EAAE,SAAS,CAAC,CAAA;AAC9D,CAAC;AAED,mFAAmF;AACnF,SAAS,aAAa,CAAC,KAAc,EAAE,KAAoB;IACzD,IAAI,KAAK,CAAC,EAAE,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IACvC,MAAM,GAAG,GAAG,gBAAgB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IACxC,MAAM,EAAE,GAAG,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IAC9B,OAAO,EAAE,KAAK,SAAS,IAAI,UAAU,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,SAAS,CAAA;AAChE,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,eAAe,CAAC,KAAc,EAAE,MAAgC;IAC9E,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,aAAa,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAA;IACtE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,KAAK,EAAE,CAAA;IAC1C,MAAM,QAAQ,GAAgB,EAAE,UAAU,EAAE,YAAY,EAAE,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,EAAE,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,EAAE,CAAA;IAC/G,MAAM,MAAM,GAAG,oBAAoB,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAA;IACrE,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,SAAS,CAAA;IAC/C,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,CAAA;AACxD,CAAC;AAED,6FAA6F;AAC7F,MAAM,UAAU,YAAY,CAAC,KAAuB,EAAE,MAAgC;IACpF,MAAM,QAAQ,GAAG,eAAe,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,CAAA;IAC9D,OAAO,QAAQ,CAAC,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;AACxE,CAAC;AAED;;;;;GAKG;AACH,SAAS,WAAW,CAClB,OAAyB,EACzB,KAAkC,EAClC,MAAgC;IAEhC,MAAM,GAAG,GAAuB,EAAE,CAAA;IAClC,IAAI,SAAS,GAAG,aAAa,CAAC,OAAO,CAAC,CAAA;IACtC,KAAK,MAAM,KAAK,IAAI,KAAK,EAAE,CAAC;QAC1B,MAAM,QAAQ,GAAG,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;QAC5C,IAAI,CAAC,QAAQ;YAAE,MAAK;QACpB,MAAM,KAAK,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAA;QACrC,IAAI,kBAAkB,CAAC,KAAK,EAAE,SAAS,CAAC;YAAE,SAAQ;QAClD,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QAClB,SAAS,GAAG,KAAK,CAAA;IACnB,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAeD;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,KAAqB,EAAE,IAAa,EAAE,EAAW;IAC5E,MAAM,MAAM,GAAG,cAAc,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;IACvC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAA;IACzC,MAAM,OAAO,GAAqB,EAAE,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAA;IACrF,MAAM,IAAI,GAAG,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,IAAI,YAAY,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC,CAAA;IAC7E,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,MAAM,CAAC,CAAC,OAAO,EAAE,CAAA;IAC3E,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACtD,MAAM,cAAc,GAAG,kBAAkB,CACvC,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EACxC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CACxC,CAAA;IACD,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC,CAAA;IAC1D,OAAO;QACL,OAAO,EAAE,IAAI,CAAC,IAAI;QAClB,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,IAAI;QACJ,MAAM;QACN,UAAU,EAAE,kBAAkB,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,UAAU,CAAC;QAC1E,WAAW,EACT,KAAK,CAAC,WAAW,KAAK,IAAI,IAAI,cAAc,IAAI,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,WAAW,CAAC;YAChG,CAAC,CAAC,KAAK,CAAC,WAAW;YACnB,CAAC,CAAC,IAAI;QACV,SAAS,EAAE,KAAK,CAAC,SAAS,KAAK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI;KACpG,CAAA;AACH,CAAC;AA2DD;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAgB,EAAE,QAA4B,EAAE,OAAO,GAAG,CAAC;IAC1F,IAAI,MAAM,GAAG,OAAO,CAAA;IACpB,IAAI,QAAQ,GAAG,QAAQ,CAAA;IACvB,IAAI,IAAI,GAAG,OAAO,CAAA;IAClB,IAAI,UAAU,GAAG,CAAC,CAAA;IAClB,OAAO;QACL,OAAO,CAAC,MAAM;YACZ,IAAI,MAAM,CAAC,GAAG,IAAI,IAAI;gBAAE,OAAO,SAAS,CAAA;YACxC,IAAI,GAAG,MAAM,CAAC,GAAG,CAAA;YACjB,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAA;YAC1B,MAAM,KAAK,GAAG,cAAc,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,CAAA;YACpD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,SAAS,CAAA;YACxC,MAAM,GAAG,eAAe,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,IAAI,MAAM,CAAC,EAAE,CAAA;YAC3D,UAAU,IAAI,CAAC,CAAA;YACf,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,CAAA;QAC7D,CAAC;QACD,MAAM,CAAC,KAAK,EAAE,OAAO;YACnB,IAAI,OAAO,GAAG,UAAU;gBAAE,OAAO,SAAS,CAAA;YAC1C,IAAI,kBAAkB,CAAC,KAAK,EAAE,MAAM,CAAC;gBAAE,OAAO,SAAS,CAAA;YACvD,MAAM,IAAI,GAAG,MAAM,CAAA;YACnB,MAAM,GAAG,KAAK,CAAA;YACd,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAA;QAC5C,CAAC;KACF,CAAA;AACH,CAAC;AAED,oHAAoH;AACpH,MAAM,UAAU,kBAAkB,CAAC,MAAqB,EAAE,KAAoB,EAAE,UAAyB;IACvG,IAAI,UAAU,KAAK,IAAI;QAAE,OAAO,IAAI,CAAA;IACpC,IAAI,EAAE,GAAkB,UAAU,CAAA;IAClC,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC;QACnB,IAAI,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;YAAE,OAAO,EAAE,CAAA;QACjC,EAAE,GAAG,aAAa,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,IAAI,IAAI,CAAA;IACnD,CAAC;IACD,OAAO,KAAK,CAAC,EAAE,CAAA;AACjB,CAAC","sourcesContent":["import { applyDocumentPatches, locateNode, semanticValueEqual, valuePathNodeIds } from '@elytracms/core/cms-core'\nimport type { CmsDocument, DocumentPatch } from '@elytracms/core/cms-core'\nimport { DEFAULT_SLOT, getNode, getParentInfo, mergeBlockRoots, splitBlockRoots } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { compositionPatches } from './composition-patches'\nimport { runByKey } from './canvas-runs'\nimport type { PreservedItem } from './canvas-runs'\n\n/**\n * EC-630 — the canvas REBASES onto a change of its field made underneath it (a colleague's\n * patch arriving by subscription, the canonical row a write returned, a resolved\n * conflict) instead of remounting: the change lands, and the editor's undo history and\n * selection survive (EC-595's model — the canvas commits and rebases like every other\n * editor). Pure; the builder reducer's `rebase` action calls {@link rebaseCanvas}.\n *\n * **The external change** is named by the document draft that saw it happen: the field's\n * value before (`from`) and after (`to`) the session's document moved. Its delta is the\n * one the canvas writer itself would emit between the two ({@link externalChange} — node\n * patches keyed by id, or the coarser whole-field write when node verbs cannot say it: a\n * typography change, prose being one position). Measured between two DRAFT values, the\n * delta holds the colleague's change and nothing else — not this editor's own edits (both\n * values hold them), not a canonicalisation of them.\n *\n * **The present absorbs the change** with THE merge (`applyDocumentPatches`, the function\n * both stores run), so an edit of this editor's that the draft has not seen yet survives\n * under it; when the present cannot take it, the incoming value wins. Never re-normalised\n * (receiving a change never writes one back).\n *\n * **Undo never reverts an external change.** The history holds whole-state snapshots, so\n * kept as they were, an undo would restore a pre-change snapshot and the next commit\n * would write patches that silently revert the colleague. Instead every snapshot ABSORBS\n * the same delta: an undo then restores the editor's own earlier state WITH the\n * colleague's change in it.\n * - A `remove` of a node the snapshot does not hold is already absorbed (nothing to\n * resurrect).\n * - Any other patch a snapshot cannot take (its node is not there — the colleague edited\n * a node this editor inserted later, or moved one into it) means restoring it WOULD\n * revert the colleague: the snapshot and every entry beyond it (older undo steps, later\n * redo steps — reachable only through it) are dropped. Conservative: a lost undo step\n * is recoverable, a silently destroyed colleague's edit is not.\n * - Steps the change emptied (an undo that would now change nothing — the colleague wrote\n * the same position later, or rewrote the whole field) are dropped, so every remaining\n * undo does something.\n *\n * **The selection** survives while its node does. When the change removed it, it moves to\n * the nearest surviving ancestor (the rule a local `remove` follows — the inspector stays\n * where the editor was working), else the root. A selected typography RUN stays selected\n * only while the typography is unchanged: runs carry no ids (EC-500 D1), so after a prose\n * change a key no longer names the same run with certainty.\n */\n\n/** The field name the canvas's own deltas are addressed at (internal — never stored, never sent). */\nconst CANVAS_FIELD = 'canvas'\n\n/** One composition state: the synthetic root and the preserved typography (a history entry's shape). */\nexport interface CanvasValueState {\n readonly root: ComponentNode\n readonly preserved: readonly PreservedItem[]\n}\n\n/** The stored v2 sequence a composition state stands for (THE merge order). */\nexport function canvasValueOf(state: CanvasValueState): unknown {\n return mergeBlockRoots(state.root.slots?.[DEFAULT_SLOT] ?? [], state.preserved)\n}\n\n/** A composition state holding `value` under the given synthetic root (its children replaced). */\nfunction stateOfValue(root: ComponentNode, value: unknown): CanvasValueState {\n const split = splitBlockRoots(value)\n return { root: { ...root, slots: { ...root.slots, [DEFAULT_SLOT]: split.roots } }, preserved: split.preserved }\n}\n\n/** The external change between two values of the field (the canvas writer's delta; `[]` when equal). */\nexport function externalChange(from: unknown, to: unknown): DocumentPatch[] {\n return compositionPatches(from, to, CANVAS_FIELD, undefined)\n}\n\n/** Whether a `remove` names a node this value does not hold (already absorbed). */\nfunction removesAbsent(value: unknown, patch: DocumentPatch): boolean {\n if (patch.op !== 'remove') return false\n const ids = valuePathNodeIds(patch.path)\n const id = ids[ids.length - 1]\n return id !== undefined && locateNode(value, id) === undefined\n}\n\n/** A field value with the change absorbed, or `undefined` when it cannot take it. */\nexport function absorbIntoValue(value: unknown, change: readonly DocumentPatch[]): { value: unknown } | undefined {\n const patches = change.filter((patch) => !removesAbsent(value, patch))\n if (patches.length === 0) return { value }\n const document: CmsDocument = { collection: CANVAS_FIELD, id: CANVAS_FIELD, values: { [CANVAS_FIELD]: value } }\n const result = applyDocumentPatches({ document, patches, since: [] })\n if (result.refused.length > 0) return undefined\n return { value: result.document.values[CANVAS_FIELD] }\n}\n\n/** One composition state with the change absorbed, or `undefined` when it cannot take it. */\nexport function absorbChange(state: CanvasValueState, change: readonly DocumentPatch[]): CanvasValueState | undefined {\n const absorbed = absorbIntoValue(canvasValueOf(state), change)\n return absorbed ? stateOfValue(state.root, absorbed.value) : undefined\n}\n\n/**\n * Rebase one direction of the history (`chain` ordered from the present outward — the\n * newest undo step first, or the next redo step first): each entry absorbs the change,\n * the first that cannot ends the chain, and an entry equal to its neighbour toward the\n * present is dropped. Returns the chain in the same order.\n */\nfunction rebaseChain(\n present: CanvasValueState,\n chain: readonly CanvasValueState[],\n change: readonly DocumentPatch[],\n): CanvasValueState[] {\n const out: CanvasValueState[] = []\n let neighbour = canvasValueOf(present)\n for (const entry of chain) {\n const absorbed = absorbChange(entry, change)\n if (!absorbed) break\n const value = canvasValueOf(absorbed)\n if (semanticValueEqual(value, neighbour)) continue\n out.push(absorbed)\n neighbour = value\n }\n return out\n}\n\n/** The editor state a rebase reads and writes (the builder state's composition, history and selection). */\nexport interface RebasableState {\n readonly present: ComponentNode\n readonly preserved: readonly PreservedItem[]\n /** Oldest first. */\n readonly past: readonly CanvasValueState[]\n /** Next redo first. */\n readonly future: readonly CanvasValueState[]\n readonly selectedId: string | null\n readonly selectedRun: number | null\n readonly hoveredId: string | null\n}\n\n/**\n * Rebase the editor onto the field's change `from` → `to` (see the module doc). Returns\n * `undefined` when there is nothing to change.\n */\nexport function rebaseCanvas(state: RebasableState, from: unknown, to: unknown): RebasableState | undefined {\n const change = externalChange(from, to)\n if (change.length === 0) return undefined\n const current: CanvasValueState = { root: state.present, preserved: state.preserved }\n const next = absorbChange(current, change) ?? stateOfValue(state.present, to)\n const past = rebaseChain(next, [...state.past].reverse(), change).reverse()\n const future = rebaseChain(next, state.future, change)\n const typographyKept = semanticValueEqual(\n state.preserved.map((item) => item.node),\n next.preserved.map((item) => item.node),\n )\n const roots = next.root.slots?.[DEFAULT_SLOT]?.length ?? 0\n return {\n present: next.root,\n preserved: next.preserved,\n past,\n future,\n selectedId: survivingSelection(state.present, next.root, state.selectedId),\n selectedRun:\n state.selectedRun !== null && typographyKept && runByKey(roots, next.preserved, state.selectedRun)\n ? state.selectedRun\n : null,\n hoveredId: state.hoveredId !== null && getNode(next.root, state.hoveredId) ? state.hoveredId : null,\n }\n}\n\n/**\n * A change of the canvas field's value made UNDERNEATH the canvas, as the document draft\n * saw it happen: the field's value before and after the session's document moved, a\n * sequence number (one per change, increasing), and the revision the draft is at after it\n * (EC-647). `from` ≡ `to` when only the revision moved — the field held still while the\n * document advanced (a write of another field landed, the canvas's own write was\n * acknowledged): the canvas's agreed value is then valid at the newer revision too.\n */\nexport interface CanvasFieldChange {\n readonly from: unknown\n readonly to: unknown\n readonly seq: number\n readonly revision: number | undefined\n}\n\n/**\n * What the canvas lifts into the draft: its value, the value its delta is measured from,\n * and the revision that base value was agreed at — the edit's AUTHORING revision (EC-647:\n * a value and its revision travel together; the draft's revision may already be newer\n * than anything the canvas has seen).\n */\nexport interface CanvasLift {\n readonly value: unknown\n /** The value the store last agreed on with the draft — the canvas writer's delta is `base` → `value`. */\n readonly base: unknown\n /** The revision `base` was agreed at (`undefined`: the draft's view had none). */\n readonly revision: number | undefined\n}\n\n/**\n * EC-630 — the two-way sync between the canvas store and the document draft (pure; the\n * canvas's `CompositionSync` is its React shell).\n *\n * It tracks the value the store and the draft last AGREED on (`synced`) and — EC-647 —\n * the revision it was agreed at, as one value:\n * - {@link CanvasSync.stored} — the store's value changed (an edit, an undo, a rebase's\n * adoption): lift it, with `synced` as the base of the canvas writer's delta and its\n * revision as the edit's authoring revision, unless the draft already holds it.\n * Measuring the delta from `synced` — never from the draft's current value — is what\n * keeps a colleague's change that reached the draft a moment before an edit lifts out\n * of that edit's patches (they would otherwise revert it); pairing it with `synced`'s\n * revision — never the draft's — is what makes the store refuse that edit where it\n * overlaps the colleague's change (Codex F12), instead of accepting it at a revision\n * that already holds a change the canvas never saw. The canvas's own lift keeps the\n * revision (its own edit is not a revision of the document).\n * - {@link CanvasSync.changed} — the draft reports a change made underneath: `synced`\n * absorbs it AND takes the change's revision (a rebase rebases the value and the\n * revision), and the action to dispatch is a `rebase` — none when only the revision\n * moved. Until the store has applied every rebase dispatched (its `rebased` count), a\n * store value predates one and is not lifted (its edits are lifted from the rebased\n * value right after).\n */\nexport interface CanvasSync {\n changed(change: CanvasFieldChange): { type: 'rebase'; from: unknown; to: unknown } | undefined\n stored(value: unknown, rebased: number): CanvasLift | undefined\n}\n\n/**\n * The sync of a canvas mounted on `initial` — the draft's value of the field at `revision`\n * (the revision the draft was at when the canvas mounted) — having seen the draft's\n * changes up to `seenSeq`.\n */\nexport function createCanvasSync(initial: unknown, revision: number | undefined, seenSeq = 0): CanvasSync {\n let synced = initial\n let agreedAt = revision\n let seen = seenSeq\n let dispatched = 0\n return {\n changed(change) {\n if (change.seq <= seen) return undefined\n seen = change.seq\n agreedAt = change.revision\n const delta = externalChange(change.from, change.to)\n if (delta.length === 0) return undefined\n synced = absorbIntoValue(synced, delta)?.value ?? change.to\n dispatched += 1\n return { type: 'rebase', from: change.from, to: change.to }\n },\n stored(value, rebased) {\n if (rebased < dispatched) return undefined\n if (semanticValueEqual(value, synced)) return undefined\n const base = synced\n synced = value\n return { value, base, revision: agreedAt }\n },\n }\n}\n\n/** The selection after a rebase: kept while its node exists, else its nearest surviving ancestor, else the root. */\nexport function survivingSelection(before: ComponentNode, after: ComponentNode, selectedId: string | null): string | null {\n if (selectedId === null) return null\n let id: string | null = selectedId\n while (id !== null) {\n if (getNode(after, id)) return id\n id = getParentInfo(before, id)?.parent.id ?? null\n }\n return after.id\n}\n"]}
@@ -2,11 +2,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useMemo, useSyncExternalStore } from 'react';
3
3
  import { DRAFT_PERSPECTIVE } from '@elytracms/core/cms-core';
4
4
  import { resolvedAsset } from '@elytracms/core/content';
5
- import { assetReadLocale } from '@elytracms/core/persistence';
6
5
  import { createRenderContext, listingFailureOf } from '@elytracms/core/runtime-renderer';
7
6
  import { translate } from '../../lib/i18n/index.js';
8
7
  import { contentQueryErrorMessage } from '../../lib/persona/labels.js';
9
8
  import { acquireContentSessionConsumer, contentSessionEpoch, resolveCanvasLink, resolveCanvasListing, resolveCanvasRelation, subscribeContentSession, validateCanvasListingQuery, } from '../../lib/data/index.js';
9
+ import { useAssetReadLocale } from '../assets/asset-locale.js';
10
10
  import { useStudioLocale } from '../../lib/locale/index.js';
11
11
  import { useOptionalAssets } from '../assets/assets-store.js';
12
12
  import { useReuse } from './reuse-context.js';
@@ -149,12 +149,13 @@ export function useCanvasRenderContext(args) {
149
149
  // the content engine, which then resolves in the SESSION's own default locale —
150
150
  // never a fabricated one — so a canvas mounted outside a CMS session still previews
151
151
  // in the project's locale.
152
- const { locale, config: localeConfig } = useStudioLocale();
152
+ const { locale } = useStudioLocale();
153
153
  const activeLocale = locale ?? undefined;
154
154
  // EC-632: an asset's localized metadata (alt) reads in the same locale, falling back to
155
155
  // the project's default — `null` (shared metadata only) before a config is installed.
156
- const defaultLocale = localeConfig?.default;
157
- const assetLocale = useMemo(() => (defaultLocale === undefined ? null : assetReadLocale({ default: defaultLocale }, activeLocale)), [defaultLocale, activeLocale]);
156
+ // EC-659: the studio's ONE asset read locale, read through the project's asset collection
157
+ // as declared now — a translation of a field it no longer localizes is never previewed.
158
+ const assetLocale = useAssetReadLocale();
158
159
  const assetsStore = useOptionalAssets();
159
160
  const resolveAsset = useCallback((assetId) => canvasRenderAsset(assetsStore?.assets.find((a) => a.id === assetId), assetLocale), [assetsStore, assetLocale]);
160
161
  // EC-300: the reuse capability identifies a transcluding embed + its relation prop,