@elytracms/studio 0.0.91 → 0.0.93

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 (353) hide show
  1. package/convex/asset-alt-migration.test.ts +127 -0
  2. package/convex/asset-replace.test.ts +168 -0
  3. package/convex/asset-revalidate.test.ts +6 -6
  4. package/convex/assets.ts +41 -5
  5. package/convex/cms.ts +537 -177
  6. package/convex/contract-parity.test.ts +173 -19
  7. package/convex/delivery.ts +26 -13
  8. package/convex/document-revalidate.test.ts +17 -16
  9. package/convex/orphan-hydration.test.ts +116 -0
  10. package/convex/presence-cost.test.ts +89 -0
  11. package/convex/rail-write-parity.test.ts +17 -14
  12. package/convex/referenceMaintenance.ts +15 -15
  13. package/convex/schema.ts +70 -5
  14. package/convex/triggers.ts +21 -8
  15. package/convex/write-limits.ts +1 -1
  16. package/dist/components/ai/ai-chat-panel.js +33 -22
  17. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  18. package/dist/components/ai/proposal-card.js +2 -1
  19. package/dist/components/ai/proposal-card.js.map +1 -1
  20. package/dist/components/ai/translate-document-action.d.ts +8 -2
  21. package/dist/components/ai/translate-document-action.js.map +1 -1
  22. package/dist/components/ai/translate-document-dialog.d.ts +1 -1
  23. package/dist/components/ai/translate-document-dialog.js +14 -8
  24. package/dist/components/ai/translate-document-dialog.js.map +1 -1
  25. package/dist/components/ai/translate-skipped.d.ts +19 -0
  26. package/dist/components/ai/translate-skipped.js +46 -0
  27. package/dist/components/ai/translate-skipped.js.map +1 -0
  28. package/dist/components/app-views/default-modules.d.ts +23 -6
  29. package/dist/components/app-views/default-modules.js +33 -14
  30. package/dist/components/app-views/default-modules.js.map +1 -1
  31. package/dist/components/assets/asset-detail.js +18 -8
  32. package/dist/components/assets/asset-detail.js.map +1 -1
  33. package/dist/components/assets/asset-metadata.d.ts +15 -4
  34. package/dist/components/assets/asset-metadata.js +15 -2
  35. package/dist/components/assets/asset-metadata.js.map +1 -1
  36. package/dist/components/assets/asset-search.js +4 -1
  37. package/dist/components/assets/asset-search.js.map +1 -1
  38. package/dist/components/assets/asset-selection.d.ts +0 -8
  39. package/dist/components/assets/asset-selection.js +4 -23
  40. package/dist/components/assets/asset-selection.js.map +1 -1
  41. package/dist/components/assets/asset-thumb.js +3 -2
  42. package/dist/components/assets/asset-thumb.js.map +1 -1
  43. package/dist/components/assets/assets-collection-view-source.d.ts +3 -3
  44. package/dist/components/assets/assets-collection-view-source.js +9 -5
  45. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  46. package/dist/components/assets/assets-store.d.ts +14 -0
  47. package/dist/components/assets/assets-store.js +23 -1
  48. package/dist/components/assets/assets-store.js.map +1 -1
  49. package/dist/components/assets/replace-file.d.ts +22 -0
  50. package/dist/components/assets/replace-file.js +59 -0
  51. package/dist/components/assets/replace-file.js.map +1 -0
  52. package/dist/components/builder/blocks-model.d.ts +14 -26
  53. package/dist/components/builder/blocks-model.js +35 -41
  54. package/dist/components/builder/blocks-model.js.map +1 -1
  55. package/dist/components/builder/blocks-panel.d.ts +7 -3
  56. package/dist/components/builder/blocks-panel.js +6 -5
  57. package/dist/components/builder/blocks-panel.js.map +1 -1
  58. package/dist/components/builder/builder-store.d.ts +126 -30
  59. package/dist/components/builder/builder-store.js +169 -67
  60. package/dist/components/builder/builder-store.js.map +1 -1
  61. package/dist/components/builder/canvas-body.d.ts +30 -0
  62. package/dist/components/builder/canvas-body.js +38 -0
  63. package/dist/components/builder/canvas-body.js.map +1 -0
  64. package/dist/components/builder/canvas-chrome-css.js +7 -0
  65. package/dist/components/builder/canvas-chrome-css.js.map +1 -1
  66. package/dist/components/builder/canvas-dnd.d.ts +7 -7
  67. package/dist/components/builder/canvas-dnd.js +5 -5
  68. package/dist/components/builder/canvas-dnd.js.map +1 -1
  69. package/dist/components/builder/canvas-render-context.d.ts +30 -22
  70. package/dist/components/builder/canvas-render-context.js +44 -44
  71. package/dist/components/builder/canvas-render-context.js.map +1 -1
  72. package/dist/components/builder/canvas-runs.d.ts +45 -0
  73. package/dist/components/builder/canvas-runs.js +51 -0
  74. package/dist/components/builder/canvas-runs.js.map +1 -0
  75. package/dist/components/builder/canvas.d.ts +5 -4
  76. package/dist/components/builder/canvas.js +34 -6
  77. package/dist/components/builder/canvas.js.map +1 -1
  78. package/dist/components/builder/clipboard-actions.d.ts +15 -0
  79. package/dist/components/builder/clipboard-actions.js +50 -13
  80. package/dist/components/builder/clipboard-actions.js.map +1 -1
  81. package/dist/components/builder/composition-field-editor.d.ts +10 -16
  82. package/dist/components/builder/composition-field-editor.js +30 -40
  83. package/dist/components/builder/composition-field-editor.js.map +1 -1
  84. package/dist/components/builder/composition-patches.d.ts +7 -0
  85. package/dist/components/builder/composition-patches.js +256 -0
  86. package/dist/components/builder/composition-patches.js.map +1 -0
  87. package/dist/components/builder/delete-key.d.ts +5 -0
  88. package/dist/components/builder/delete-key.js +4 -0
  89. package/dist/components/builder/delete-key.js.map +1 -1
  90. package/dist/components/builder/insert-context.d.ts +17 -25
  91. package/dist/components/builder/insert-context.js +45 -92
  92. package/dist/components/builder/insert-context.js.map +1 -1
  93. package/dist/components/builder/page-tree.d.ts +3 -3
  94. package/dist/components/builder/page-tree.js +3 -3
  95. package/dist/components/builder/page-tree.js.map +1 -1
  96. package/dist/components/builder/placement-refusal.d.ts +18 -0
  97. package/dist/components/builder/placement-refusal.js +46 -0
  98. package/dist/components/builder/placement-refusal.js.map +1 -0
  99. package/dist/components/builder/property-inspector.js +4 -0
  100. package/dist/components/builder/property-inspector.js.map +1 -1
  101. package/dist/components/builder/reuse-context.js +65 -26
  102. package/dist/components/builder/reuse-context.js.map +1 -1
  103. package/dist/components/builder/reuse.d.ts +79 -8
  104. package/dist/components/builder/reuse.js +71 -23
  105. package/dist/components/builder/reuse.js.map +1 -1
  106. package/dist/components/builder/run-inspector.d.ts +11 -0
  107. package/dist/components/builder/run-inspector.js +33 -0
  108. package/dist/components/builder/run-inspector.js.map +1 -0
  109. package/dist/components/cms/asset-field.d.ts +38 -1
  110. package/dist/components/cms/asset-field.js +182 -58
  111. package/dist/components/cms/asset-field.js.map +1 -1
  112. package/dist/components/cms/asset-picker-core.d.ts +16 -0
  113. package/dist/components/cms/asset-picker-core.js +20 -0
  114. package/dist/components/cms/asset-picker-core.js.map +1 -1
  115. package/dist/components/cms/asset-picker.js +12 -18
  116. package/dist/components/cms/asset-picker.js.map +1 -1
  117. package/dist/components/cms/bulk-actions.d.ts +4 -20
  118. package/dist/components/cms/bulk-actions.js +3 -24
  119. package/dist/components/cms/bulk-actions.js.map +1 -1
  120. package/dist/components/cms/cell-edit.d.ts +5 -5
  121. package/dist/components/cms/cell-edit.js.map +1 -1
  122. package/dist/components/cms/cell.js.map +1 -1
  123. package/dist/components/cms/cms-store.d.ts +87 -45
  124. package/dist/components/cms/cms-store.js +347 -203
  125. package/dist/components/cms/cms-store.js.map +1 -1
  126. package/dist/components/cms/collection-table.js +7 -2
  127. package/dist/components/cms/collection-table.js.map +1 -1
  128. package/dist/components/cms/commit-policy.d.ts +24 -0
  129. package/dist/components/cms/commit-policy.js +37 -0
  130. package/dist/components/cms/commit-policy.js.map +1 -0
  131. package/dist/components/cms/detail-editor.js +83 -137
  132. package/dist/components/cms/detail-editor.js.map +1 -1
  133. package/dist/components/cms/document-diff-view.d.ts +3 -1
  134. package/dist/components/cms/document-diff-view.js +12 -7
  135. package/dist/components/cms/document-diff-view.js.map +1 -1
  136. package/dist/components/cms/draft-commit.d.ts +70 -0
  137. package/dist/components/cms/draft-commit.js +82 -0
  138. package/dist/components/cms/draft-commit.js.map +1 -0
  139. package/dist/components/cms/duplicate-document.d.ts +11 -13
  140. package/dist/components/cms/duplicate-document.js +26 -34
  141. package/dist/components/cms/duplicate-document.js.map +1 -1
  142. package/dist/components/cms/field-anchors.d.ts +7 -4
  143. package/dist/components/cms/field-anchors.js +9 -7
  144. package/dist/components/cms/field-anchors.js.map +1 -1
  145. package/dist/components/cms/field-collaboration.d.ts +47 -0
  146. package/dist/components/cms/field-collaboration.js +87 -0
  147. package/dist/components/cms/field-collaboration.js.map +1 -0
  148. package/dist/components/cms/field-control-lib.d.ts +22 -1
  149. package/dist/components/cms/field-control-lib.js +33 -4
  150. package/dist/components/cms/field-control-lib.js.map +1 -1
  151. package/dist/components/cms/field-control.js +17 -14
  152. package/dist/components/cms/field-control.js.map +1 -1
  153. package/dist/components/cms/gate-validation.d.ts +21 -30
  154. package/dist/components/cms/gate-validation.js +11 -22
  155. package/dist/components/cms/gate-validation.js.map +1 -1
  156. package/dist/components/cms/object-array-field.js +4 -4
  157. package/dist/components/cms/object-array-field.js.map +1 -1
  158. package/dist/components/cms/prop-fields.d.ts +7 -2
  159. package/dist/components/cms/prop-fields.js +13 -6
  160. package/dist/components/cms/prop-fields.js.map +1 -1
  161. package/dist/components/cms/reference-confirm.js +10 -4
  162. package/dist/components/cms/reference-confirm.js.map +1 -1
  163. package/dist/components/cms/rich-text/embed-picker.d.ts +1 -1
  164. package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
  165. package/dist/components/cms/rich-text/insert.d.ts +20 -9
  166. package/dist/components/cms/rich-text/insert.js +25 -17
  167. package/dist/components/cms/rich-text/insert.js.map +1 -1
  168. package/dist/components/cms/use-document-draft.d.ts +96 -0
  169. package/dist/components/cms/use-document-draft.js +283 -0
  170. package/dist/components/cms/use-document-draft.js.map +1 -0
  171. package/dist/components/cms/use-document-versions.js +2 -2
  172. package/dist/components/cms/use-document-versions.js.map +1 -1
  173. package/dist/components/cms/use-live-url.d.ts +6 -12
  174. package/dist/components/cms/use-live-url.js +16 -15
  175. package/dist/components/cms/use-live-url.js.map +1 -1
  176. package/dist/components/cms/use-publish-controller.d.ts +13 -18
  177. package/dist/components/cms/use-publish-controller.js +24 -41
  178. package/dist/components/cms/use-publish-controller.js.map +1 -1
  179. package/dist/components/cms/use-published-perspective.d.ts +2 -3
  180. package/dist/components/cms/use-published-perspective.js +4 -3
  181. package/dist/components/cms/use-published-perspective.js.map +1 -1
  182. package/dist/components/cms/versions-pane.js +8 -1
  183. package/dist/components/cms/versions-pane.js.map +1 -1
  184. package/dist/components/collection-view/cms-collection-view-source.js +24 -7
  185. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  186. package/dist/components/components/transfer-dialog.js +28 -23
  187. package/dist/components/components/transfer-dialog.js.map +1 -1
  188. package/dist/components/components/transfer.d.ts +31 -21
  189. package/dist/components/components/transfer.js +49 -35
  190. package/dist/components/components/transfer.js.map +1 -1
  191. package/dist/components/project-list.js.map +1 -1
  192. package/dist/components/project-session.d.ts +6 -3
  193. package/dist/components/project-session.js +14 -8
  194. package/dist/components/project-session.js.map +1 -1
  195. package/dist/components/project-switcher.js.map +1 -1
  196. package/dist/components/rail-view-source.js +2 -1
  197. package/dist/components/rail-view-source.js.map +1 -1
  198. package/dist/components/section-nav.js +1 -1
  199. package/dist/components/section-nav.js.map +1 -1
  200. package/dist/lib/ai/apply-content.js +56 -116
  201. package/dist/lib/ai/apply-content.js.map +1 -1
  202. package/dist/lib/ai/chat-context.d.ts +8 -0
  203. package/dist/lib/ai/chat-context.js +37 -6
  204. package/dist/lib/ai/chat-context.js.map +1 -1
  205. package/dist/lib/ai/chat-state.d.ts +14 -1
  206. package/dist/lib/ai/chat-state.js.map +1 -1
  207. package/dist/lib/ai/constraints.d.ts +20 -5
  208. package/dist/lib/ai/constraints.js +125 -33
  209. package/dist/lib/ai/constraints.js.map +1 -1
  210. package/dist/lib/ai/fake-provider.d.ts +16 -3
  211. package/dist/lib/ai/fake-provider.js +22 -6
  212. package/dist/lib/ai/fake-provider.js.map +1 -1
  213. package/dist/lib/ai/index.d.ts +2 -0
  214. package/dist/lib/ai/index.js +2 -0
  215. package/dist/lib/ai/index.js.map +1 -1
  216. package/dist/lib/ai/leash.d.ts +34 -0
  217. package/dist/lib/ai/leash.js +28 -0
  218. package/dist/lib/ai/leash.js.map +1 -0
  219. package/dist/lib/ai/provenance.d.ts +1 -1
  220. package/dist/lib/ai/provenance.js.map +1 -1
  221. package/dist/lib/ai/rich-text-input.d.ts +8 -8
  222. package/dist/lib/ai/rich-text-input.js +2 -1
  223. package/dist/lib/ai/rich-text-input.js.map +1 -1
  224. package/dist/lib/ai/rich-text-translate.d.ts +27 -4
  225. package/dist/lib/ai/rich-text-translate.js +28 -10
  226. package/dist/lib/ai/rich-text-translate.js.map +1 -1
  227. package/dist/lib/ai/shape.d.ts +13 -13
  228. package/dist/lib/ai/shape.js +69 -93
  229. package/dist/lib/ai/shape.js.map +1 -1
  230. package/dist/lib/ai/tools.d.ts +36 -41
  231. package/dist/lib/ai/tools.js +85 -87
  232. package/dist/lib/ai/tools.js.map +1 -1
  233. package/dist/lib/ai/translate-document.d.ts +39 -5
  234. package/dist/lib/ai/translate-document.js +160 -51
  235. package/dist/lib/ai/translate-document.js.map +1 -1
  236. package/dist/lib/ai/types.d.ts +34 -23
  237. package/dist/lib/ai/types.js.map +1 -1
  238. package/dist/lib/ai/update-fields.d.ts +21 -12
  239. package/dist/lib/ai/update-fields.js +118 -177
  240. package/dist/lib/ai/update-fields.js.map +1 -1
  241. package/dist/lib/ai/writer-form.d.ts +116 -0
  242. package/dist/lib/ai/writer-form.js +429 -0
  243. package/dist/lib/ai/writer-form.js.map +1 -0
  244. package/dist/lib/ai-current-document.d.ts +11 -11
  245. package/dist/lib/ai-current-document.js +4 -4
  246. package/dist/lib/ai-current-document.js.map +1 -1
  247. package/dist/lib/assets/upload.d.ts +31 -2
  248. package/dist/lib/assets/upload.js +61 -6
  249. package/dist/lib/assets/upload.js.map +1 -1
  250. package/dist/lib/clipboard/os-clipboard.d.ts +10 -2
  251. package/dist/lib/clipboard/os-clipboard.js +8 -4
  252. package/dist/lib/clipboard/os-clipboard.js.map +1 -1
  253. package/dist/lib/clipboard/paste-target.d.ts +18 -9
  254. package/dist/lib/clipboard/paste-target.js +8 -7
  255. package/dist/lib/clipboard/paste-target.js.map +1 -1
  256. package/dist/lib/cms/field-patch.d.ts +15 -0
  257. package/dist/lib/cms/field-patch.js +25 -0
  258. package/dist/lib/cms/field-patch.js.map +1 -0
  259. package/dist/lib/cms/published-documents.d.ts +1 -1
  260. package/dist/lib/cms/published-documents.js +1 -1
  261. package/dist/lib/cms/published-documents.js.map +1 -1
  262. package/dist/lib/cms-views/view-model.d.ts +3 -3
  263. package/dist/lib/collection-view/__fixtures__/document-writes.d.ts +6 -0
  264. package/dist/lib/collection-view/__fixtures__/document-writes.js +23 -0
  265. package/dist/lib/collection-view/__fixtures__/document-writes.js.map +1 -0
  266. package/dist/lib/collection-view/collection-view-source.d.ts +69 -6
  267. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  268. package/dist/lib/data/collection-content.d.ts +4 -13
  269. package/dist/lib/data/collection-content.js +11 -5
  270. package/dist/lib/data/collection-content.js.map +1 -1
  271. package/dist/lib/dnd/drop-rules.d.ts +25 -31
  272. package/dist/lib/dnd/drop-rules.js +53 -59
  273. package/dist/lib/dnd/drop-rules.js.map +1 -1
  274. package/dist/lib/engine-version.d.ts +1 -1
  275. package/dist/lib/engine-version.js +1 -1
  276. package/dist/lib/engine-version.js.map +1 -1
  277. package/dist/lib/i18n/messages.de.js +72 -15
  278. package/dist/lib/i18n/messages.de.js.map +1 -1
  279. package/dist/lib/i18n/messages.en.d.ts +61 -12
  280. package/dist/lib/i18n/messages.en.js +76 -16
  281. package/dist/lib/i18n/messages.en.js.map +1 -1
  282. package/dist/lib/operations/__fixtures__/write-document.d.ts +30 -0
  283. package/dist/lib/operations/__fixtures__/write-document.js +16 -0
  284. package/dist/lib/operations/__fixtures__/write-document.js.map +1 -0
  285. package/dist/lib/operations/cms-persistence.d.ts +124 -31
  286. package/dist/lib/operations/cms-persistence.js +447 -68
  287. package/dist/lib/operations/cms-persistence.js.map +1 -1
  288. package/dist/lib/operations/config-manifest-lookup.d.ts +35 -0
  289. package/dist/lib/operations/config-manifest-lookup.js +56 -0
  290. package/dist/lib/operations/config-manifest-lookup.js.map +1 -0
  291. package/dist/lib/operations/project-runtime-config.d.ts +39 -0
  292. package/dist/lib/operations/project-runtime-config.js +54 -0
  293. package/dist/lib/operations/project-runtime-config.js.map +1 -0
  294. package/dist/lib/operations/publish-with-revision.d.ts +64 -0
  295. package/dist/lib/operations/publish-with-revision.js +66 -0
  296. package/dist/lib/operations/publish-with-revision.js.map +1 -0
  297. package/dist/lib/operations/runtime-validator.d.ts +32 -0
  298. package/dist/lib/operations/runtime-validator.js +18 -0
  299. package/dist/lib/operations/runtime-validator.js.map +1 -0
  300. package/dist/lib/operations/session-hydration.d.ts +31 -14
  301. package/dist/lib/operations/session-hydration.js +12 -16
  302. package/dist/lib/operations/session-hydration.js.map +1 -1
  303. package/dist/lib/operations/studio-operations.d.ts +59 -2
  304. package/dist/lib/operations/studio-operations.js +76 -14
  305. package/dist/lib/operations/studio-operations.js.map +1 -1
  306. package/dist/lib/persistence/convex-adapter.d.ts +13 -5
  307. package/dist/lib/persistence/convex-adapter.js +189 -74
  308. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  309. package/dist/lib/persistence/convex-client.d.ts +65 -5
  310. package/dist/lib/persistence/convex-client.js +106 -3
  311. package/dist/lib/persistence/convex-client.js.map +1 -1
  312. package/dist/lib/persistence/index.js +17 -18
  313. package/dist/lib/persistence/index.js.map +1 -1
  314. package/dist/lib/persona/labels.js +4 -1
  315. package/dist/lib/persona/labels.js.map +1 -1
  316. package/dist/lib/query/operations-query.d.ts +1 -1
  317. package/dist/lib/query/operations-query.js +1 -1
  318. package/dist/lib/query/operations-query.js.map +1 -1
  319. package/dist/lib/references/impact.d.ts +21 -15
  320. package/dist/lib/references/impact.js +13 -26
  321. package/dist/lib/references/impact.js.map +1 -1
  322. package/dist/lib/references/references-store.d.ts +9 -0
  323. package/dist/lib/references/references-store.js +24 -4
  324. package/dist/lib/references/references-store.js.map +1 -1
  325. package/dist/lib/routes/route-issues.d.ts +6 -6
  326. package/dist/lib/routes/route-issues.js +26 -80
  327. package/dist/lib/routes/route-issues.js.map +1 -1
  328. package/dist/lib/workspace/config-issues.js +2 -1
  329. package/dist/lib/workspace/config-issues.js.map +1 -1
  330. package/dist/lib/workspace/index.d.ts +31 -1
  331. package/dist/lib/workspace/index.js +64 -17
  332. package/dist/lib/workspace/index.js.map +1 -1
  333. package/dist/lib/workspace/switcher-source.d.ts +3 -3
  334. package/dist/lib/workspace/switcher-source.js.map +1 -1
  335. package/dist/lib/workspace/workspace-source.d.ts +2 -2
  336. package/dist/lib/workspace/workspace-source.js +2 -1
  337. package/dist/lib/workspace/workspace-source.js.map +1 -1
  338. package/package.json +3 -3
  339. package/dist/components/cms/cms-composition-validators.d.ts +0 -16
  340. package/dist/components/cms/cms-composition-validators.js +0 -133
  341. package/dist/components/cms/cms-composition-validators.js.map +0 -1
  342. package/dist/components/cms/publish-clean.d.ts +0 -28
  343. package/dist/components/cms/publish-clean.js +0 -38
  344. package/dist/components/cms/publish-clean.js.map +0 -1
  345. package/dist/components/cms/use-save-mode.d.ts +0 -35
  346. package/dist/components/cms/use-save-mode.js +0 -54
  347. package/dist/components/cms/use-save-mode.js.map +0 -1
  348. package/dist/lib/assets/asset-placements.d.ts +0 -28
  349. package/dist/lib/assets/asset-placements.js +0 -83
  350. package/dist/lib/assets/asset-placements.js.map +0 -1
  351. package/dist/lib/cms/document-gate.d.ts +0 -47
  352. package/dist/lib/cms/document-gate.js +0 -61
  353. package/dist/lib/cms/document-gate.js.map +0 -1
@@ -1,4 +1,3 @@
1
- import type { CmsDocument } from '@elytracms/core/cms-core';
2
1
  /**
3
2
  * The current-document seam (EC-378) — a framework-free module store, like the
4
3
  * AI panel's open state (`lib/ai-panel`). The document editor (`EditorBody`)
@@ -8,12 +7,13 @@ import type { CmsDocument } from '@elytracms/core/cms-core';
8
7
  * (advisory) AND into the tool executor's `currentDocument` thunk (the
9
8
  * deterministic fallback for `getDocument` / `updateFields`).
10
9
  *
11
- * `adoptDraft` is the narrow write-back half of the seam: after the panel
12
- * applies an UPDATE proposal to the open document, it must replace the
13
- * editor's draft buffer (the editor clones its document once at mount —
14
- * without adoption, its autosave would write the stale buffer back and
15
- * silently revert the applied change). It is exactly the editor's own
16
- * `replaceDraft` (the same channel the translate dialog uses via `onApplied`).
10
+ * `flush` is the narrow write half of the seam (EC-595 A2): the FORCED FLUSH before
11
+ * an AI proposal reads or writes the open document — the editor commits its
12
+ * uncommitted fields and the persister lands them, so the assistant reads (and
13
+ * patches) the committed draft. There is no draft to hand back any more: an applied
14
+ * proposal is committed as patches to the session, and the open editor rebases on the
15
+ * session's document (its own uncommitted fields win), so a stale buffer can no longer
16
+ * write an applied change back over (the retired `adoptDraft` channel).
17
17
  *
18
18
  * This module imports NO AI runtime — publishing the tap from the always-
19
19
  * mounted editor never pulls the AI chunk (the heavy panel stays behind its
@@ -26,8 +26,8 @@ export interface AiCurrentDocument {
26
26
  id: string;
27
27
  /** Resolved display title (advisory — for the model's context block). */
28
28
  title: string;
29
- /** Replace the open editor's draft buffer with an externally applied update. */
30
- adoptDraft: (next: CmsDocument) => void;
29
+ /** The forced flush: commit the open editor's uncommitted fields and await persistence. */
30
+ flush: () => Promise<void>;
31
31
  }
32
32
  /** The document currently open in the editor, or null. */
33
33
  export declare function getAiCurrentDocument(): AiCurrentDocument | null;
@@ -42,12 +42,12 @@ export declare function clearAiCurrentDocument(ref: {
42
42
  export declare function useAiCurrentDocument(): AiCurrentDocument | null;
43
43
  /**
44
44
  * The editor-side tap: publish the open document while mounted, clear on
45
- * unmount. `adoptDraft` is kept fresh through a ref so the published value only
45
+ * unmount. `flush` is kept fresh through a ref so the published value only
46
46
  * changes when the coordinates/title do (no churn per keystroke elsewhere).
47
47
  */
48
48
  export declare function useAiCurrentDocumentTap(input: {
49
49
  collection: string;
50
50
  id: string;
51
51
  title: string;
52
- adoptDraft: (next: CmsDocument) => void;
52
+ flush: () => Promise<void>;
53
53
  }): void;
@@ -34,19 +34,19 @@ export function useAiCurrentDocument() {
34
34
  }
35
35
  /**
36
36
  * The editor-side tap: publish the open document while mounted, clear on
37
- * unmount. `adoptDraft` is kept fresh through a ref so the published value only
37
+ * unmount. `flush` is kept fresh through a ref so the published value only
38
38
  * changes when the coordinates/title do (no churn per keystroke elsewhere).
39
39
  */
40
40
  export function useAiCurrentDocumentTap(input) {
41
- const adoptRef = useRef(input.adoptDraft);
42
- adoptRef.current = input.adoptDraft;
41
+ const flushRef = useRef(input.flush);
42
+ flushRef.current = input.flush;
43
43
  const { collection, id, title } = input;
44
44
  useEffect(() => {
45
45
  setAiCurrentDocument({
46
46
  collection,
47
47
  id,
48
48
  title,
49
- adoptDraft: (next) => adoptRef.current(next),
49
+ flush: () => flushRef.current(),
50
50
  });
51
51
  return () => clearAiCurrentDocument({ collection, id });
52
52
  }, [collection, id, title]);
@@ -1 +1 @@
1
- {"version":3,"file":"ai-current-document.js","sourceRoot":"","sources":["../../src/lib/ai-current-document.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAA;AAkC/D,IAAI,OAAO,GAA6B,IAAI,CAAA;AAC5C,MAAM,SAAS,GAAG,IAAI,GAAG,EAAc,CAAA;AAEvC,SAAS,IAAI;IACX,KAAK,MAAM,QAAQ,IAAI,SAAS;QAAE,QAAQ,EAAE,CAAA;AAC9C,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,oBAAoB;IAClC,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,wDAAwD;AACxD,MAAM,UAAU,oBAAoB,CAAC,IAA8B;IACjE,IAAI,IAAI,KAAK,OAAO;QAAE,OAAM;IAC5B,OAAO,GAAG,IAAI,CAAA;IACd,IAAI,EAAE,CAAA;AACR,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,sBAAsB,CAAC,GAAuC;IAC5E,IAAI,OAAO,IAAI,OAAO,CAAC,UAAU,KAAK,GAAG,CAAC,UAAU,IAAI,OAAO,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,EAAE,CAAC;QAC9E,oBAAoB,CAAC,IAAI,CAAC,CAAA;IAC5B,CAAC;AACH,CAAC;AAED,SAAS,SAAS,CAAC,QAAoB;IACrC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACvB,OAAO,GAAG,EAAE;QACV,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IAC5B,CAAC,CAAA;AACH,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,oBAAoB;IAClC,OAAO,oBAAoB,CAAC,SAAS,EAAE,oBAAoB,EAAE,oBAAoB,CAAC,CAAA;AACpF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAKvC;IACC,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;IACzC,QAAQ,CAAC,OAAO,GAAG,KAAK,CAAC,UAAU,CAAA;IACnC,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,KAAK,CAAA;IACvC,SAAS,CAAC,GAAG,EAAE;QACb,oBAAoB,CAAC;YACnB,UAAU;YACV,EAAE;YACF,KAAK;YACL,UAAU,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC;SAC7C,CAAC,CAAA;QACF,OAAO,GAAG,EAAE,CAAC,sBAAsB,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,CAAA;IACzD,CAAC,EAAE,CAAC,UAAU,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC,CAAA;AAC7B,CAAC","sourcesContent":["import { useEffect, useRef, useSyncExternalStore } from 'react'\nimport type { CmsDocument } from '@elytracms/core/cms-core'\n\n/**\n * The current-document seam (EC-378) — a framework-free module store, like the\n * AI panel's open state (`lib/ai-panel`). The document editor (`EditorBody`)\n * publishes WHICH document is open; the AI chat panel — a disjoint React\n * subtree — reads it so \"this document\" / \"that element\" resolve without\n * interrogating the user: the coordinates go into the model's context\n * (advisory) AND into the tool executor's `currentDocument` thunk (the\n * deterministic fallback for `getDocument` / `updateFields`).\n *\n * `adoptDraft` is the narrow write-back half of the seam: after the panel\n * applies an UPDATE proposal to the open document, it must replace the\n * editor's draft buffer (the editor clones its document once at mount —\n * without adoption, its autosave would write the stale buffer back and\n * silently revert the applied change). It is exactly the editor's own\n * `replaceDraft` (the same channel the translate dialog uses via `onApplied`).\n *\n * This module imports NO AI runtime — publishing the tap from the always-\n * mounted editor never pulls the AI chunk (the heavy panel stays behind its\n * lazy boundary).\n */\nexport interface AiCurrentDocument {\n /** Collection id — the coordinate tools target. */\n collection: string\n /** Document id — the coordinate tools target. */\n id: string\n /** Resolved display title (advisory — for the model's context block). */\n title: string\n /** Replace the open editor's draft buffer with an externally applied update. */\n adoptDraft: (next: CmsDocument) => void\n}\n\nlet current: AiCurrentDocument | null = null\nconst listeners = new Set<() => void>()\n\nfunction emit(): void {\n for (const listener of listeners) listener()\n}\n\n/** The document currently open in the editor, or null. */\nexport function getAiCurrentDocument(): AiCurrentDocument | null {\n return current\n}\n\n/** Publish (or clear with `null`) the open document. */\nexport function setAiCurrentDocument(next: AiCurrentDocument | null): void {\n if (next === current) return\n current = next\n emit()\n}\n\n/** Clear the store ONLY if it still holds the given document (unmount race guard). */\nexport function clearAiCurrentDocument(ref: { collection: string; id: string }): void {\n if (current && current.collection === ref.collection && current.id === ref.id) {\n setAiCurrentDocument(null)\n }\n}\n\nfunction subscribe(listener: () => void): () => void {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\n/** React binding — a surface that wants to REACT to the open document. */\nexport function useAiCurrentDocument(): AiCurrentDocument | null {\n return useSyncExternalStore(subscribe, getAiCurrentDocument, getAiCurrentDocument)\n}\n\n/**\n * The editor-side tap: publish the open document while mounted, clear on\n * unmount. `adoptDraft` is kept fresh through a ref so the published value only\n * changes when the coordinates/title do (no churn per keystroke elsewhere).\n */\nexport function useAiCurrentDocumentTap(input: {\n collection: string\n id: string\n title: string\n adoptDraft: (next: CmsDocument) => void\n}): void {\n const adoptRef = useRef(input.adoptDraft)\n adoptRef.current = input.adoptDraft\n const { collection, id, title } = input\n useEffect(() => {\n setAiCurrentDocument({\n collection,\n id,\n title,\n adoptDraft: (next) => adoptRef.current(next),\n })\n return () => clearAiCurrentDocument({ collection, id })\n }, [collection, id, title])\n}\n"]}
1
+ {"version":3,"file":"ai-current-document.js","sourceRoot":"","sources":["../../src/lib/ai-current-document.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAA;AAkC/D,IAAI,OAAO,GAA6B,IAAI,CAAA;AAC5C,MAAM,SAAS,GAAG,IAAI,GAAG,EAAc,CAAA;AAEvC,SAAS,IAAI;IACX,KAAK,MAAM,QAAQ,IAAI,SAAS;QAAE,QAAQ,EAAE,CAAA;AAC9C,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,oBAAoB;IAClC,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,wDAAwD;AACxD,MAAM,UAAU,oBAAoB,CAAC,IAA8B;IACjE,IAAI,IAAI,KAAK,OAAO;QAAE,OAAM;IAC5B,OAAO,GAAG,IAAI,CAAA;IACd,IAAI,EAAE,CAAA;AACR,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,sBAAsB,CAAC,GAAuC;IAC5E,IAAI,OAAO,IAAI,OAAO,CAAC,UAAU,KAAK,GAAG,CAAC,UAAU,IAAI,OAAO,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,EAAE,CAAC;QAC9E,oBAAoB,CAAC,IAAI,CAAC,CAAA;IAC5B,CAAC;AACH,CAAC;AAED,SAAS,SAAS,CAAC,QAAoB;IACrC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACvB,OAAO,GAAG,EAAE;QACV,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IAC5B,CAAC,CAAA;AACH,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,oBAAoB;IAClC,OAAO,oBAAoB,CAAC,SAAS,EAAE,oBAAoB,EAAE,oBAAoB,CAAC,CAAA;AACpF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAKvC;IACC,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IACpC,QAAQ,CAAC,OAAO,GAAG,KAAK,CAAC,KAAK,CAAA;IAC9B,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,KAAK,CAAA;IACvC,SAAS,CAAC,GAAG,EAAE;QACb,oBAAoB,CAAC;YACnB,UAAU;YACV,EAAE;YACF,KAAK;YACL,KAAK,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,EAAE;SAChC,CAAC,CAAA;QACF,OAAO,GAAG,EAAE,CAAC,sBAAsB,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,CAAA;IACzD,CAAC,EAAE,CAAC,UAAU,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC,CAAA;AAC7B,CAAC","sourcesContent":["import { useEffect, useRef, useSyncExternalStore } from 'react'\n\n/**\n * The current-document seam (EC-378) — a framework-free module store, like the\n * AI panel's open state (`lib/ai-panel`). The document editor (`EditorBody`)\n * publishes WHICH document is open; the AI chat panel — a disjoint React\n * subtree — reads it so \"this document\" / \"that element\" resolve without\n * interrogating the user: the coordinates go into the model's context\n * (advisory) AND into the tool executor's `currentDocument` thunk (the\n * deterministic fallback for `getDocument` / `updateFields`).\n *\n * `flush` is the narrow write half of the seam (EC-595 A2): the FORCED FLUSH before\n * an AI proposal reads or writes the open document — the editor commits its\n * uncommitted fields and the persister lands them, so the assistant reads (and\n * patches) the committed draft. There is no draft to hand back any more: an applied\n * proposal is committed as patches to the session, and the open editor rebases on the\n * session's document (its own uncommitted fields win), so a stale buffer can no longer\n * write an applied change back over (the retired `adoptDraft` channel).\n *\n * This module imports NO AI runtime — publishing the tap from the always-\n * mounted editor never pulls the AI chunk (the heavy panel stays behind its\n * lazy boundary).\n */\nexport interface AiCurrentDocument {\n /** Collection id — the coordinate tools target. */\n collection: string\n /** Document id — the coordinate tools target. */\n id: string\n /** Resolved display title (advisory — for the model's context block). */\n title: string\n /** The forced flush: commit the open editor's uncommitted fields and await persistence. */\n flush: () => Promise<void>\n}\n\nlet current: AiCurrentDocument | null = null\nconst listeners = new Set<() => void>()\n\nfunction emit(): void {\n for (const listener of listeners) listener()\n}\n\n/** The document currently open in the editor, or null. */\nexport function getAiCurrentDocument(): AiCurrentDocument | null {\n return current\n}\n\n/** Publish (or clear with `null`) the open document. */\nexport function setAiCurrentDocument(next: AiCurrentDocument | null): void {\n if (next === current) return\n current = next\n emit()\n}\n\n/** Clear the store ONLY if it still holds the given document (unmount race guard). */\nexport function clearAiCurrentDocument(ref: { collection: string; id: string }): void {\n if (current && current.collection === ref.collection && current.id === ref.id) {\n setAiCurrentDocument(null)\n }\n}\n\nfunction subscribe(listener: () => void): () => void {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\n/** React binding — a surface that wants to REACT to the open document. */\nexport function useAiCurrentDocument(): AiCurrentDocument | null {\n return useSyncExternalStore(subscribe, getAiCurrentDocument, getAiCurrentDocument)\n}\n\n/**\n * The editor-side tap: publish the open document while mounted, clear on\n * unmount. `flush` is kept fresh through a ref so the published value only\n * changes when the coordinates/title do (no churn per keystroke elsewhere).\n */\nexport function useAiCurrentDocumentTap(input: {\n collection: string\n id: string\n title: string\n flush: () => Promise<void>\n}): void {\n const flushRef = useRef(input.flush)\n flushRef.current = input.flush\n const { collection, id, title } = input\n useEffect(() => {\n setAiCurrentDocument({\n collection,\n id,\n title,\n flush: () => flushRef.current(),\n })\n return () => clearAiCurrentDocument({ collection, id })\n }, [collection, id, title])\n}\n"]}
@@ -1,4 +1,5 @@
1
- import type { AssetRecord, AssetRepository, BlobStorageAdapter } from '@elytracms/core/persistence';
1
+ import type { FieldDef } from '@elytracms/core/cms-core';
2
+ import type { AssetRecord, AssetRepository, AssetWriteInput, BlobStorageAdapter } from '@elytracms/core/persistence';
2
3
  import type { ImageDimensions } from './image-dimensions.js';
3
4
  import type { GeneratedVariants } from './thumbnail.js';
4
5
  import type { UploadFailure, UploadState } from './upload-state.js';
@@ -28,6 +29,12 @@ export declare const xhrUploadTransport: UploadTransport;
28
29
  export interface UploadDeps {
29
30
  blobs: BlobStorageAdapter;
30
31
  assets: AssetRepository;
32
+ /**
33
+ * The project's asset collection fields (EC-568): the `source` declarations that map
34
+ * the file's embedded IPTC facts onto `meta` keys. Absent → no embedded metadata is
35
+ * written (a field without `source` is never filled).
36
+ */
37
+ assetFields?: readonly FieldDef[];
31
38
  /** Client-direct transport; only used when `blobs.createUploadUrl` exists. */
32
39
  transport?: UploadTransport;
33
40
  /** Injectable dimension detection; defaults to `createImageBitmap`-based. */
@@ -44,7 +51,16 @@ export interface UploadDeps {
44
51
  export interface UploadRequest {
45
52
  projectId: string;
46
53
  file: UploadableFile;
47
- alt?: string;
54
+ /**
55
+ * EC-568 — REPLACE the file inside this existing asset instead of creating one: the
56
+ * same id, the new bytes and everything derived from them (storage key, url,
57
+ * dimensions, type, size, thumb, lqip) through this one upload path; `meta` overridden
58
+ * per key where the new file carries a value (else kept); the focal point, the title
59
+ * and every other key kept. The previous blobs are kept (see EC-568 § Done).
60
+ */
61
+ replace?: {
62
+ assetId: string;
63
+ };
48
64
  /** Observe every state transition (drives progress UI). */
49
65
  onState?: (state: UploadState) => void;
50
66
  }
@@ -63,3 +79,16 @@ export type UploadResult = {
63
79
  * previous `state` to retry (the attempt counter carries over).
64
80
  */
65
81
  export declare function runUpload(deps: UploadDeps, request: UploadRequest, previous?: UploadState): Promise<UploadResult>;
82
+ /**
83
+ * The write of a file REPLACE (EC-568, the owner's ruling: "override the values with
84
+ * available metadata, or keep the existing if no metadata is available"):
85
+ * - the bytes' columns are SET — and a derived column the new file does not have is
86
+ * CLEARED (`null`), so an old thumb, lqip or dimension never outlives its file;
87
+ * - `meta` is the stored bag (a legacy `alt` column lifted in, EC-587) with each key the
88
+ * new file brings overridden; written only when the file brings something, and then
89
+ * the legacy column is cleared, as every `meta` write does;
90
+ * - the id, the focal point, the title and every other column are absent → kept.
91
+ */
92
+ export declare function replaceWrite(stored: AssetRecord, derived: Pick<AssetRecord, 'filename' | 'contentType' | 'byteSize' | 'storageKey'> & {
93
+ url: string;
94
+ }, dimensions: ImageDimensions | undefined, variants: GeneratedVariants, embedded: Record<string, unknown>): AssetWriteInput;
@@ -1,3 +1,5 @@
1
+ import { embeddedMetaValues, readEmbeddedMetadata } from '@elytracms/core/cms-core';
2
+ import { liftLegacyAlt } from '@elytracms/core/persistence';
1
3
  import { detectImageDimensions } from './image-dimensions.js';
2
4
  import { generateVariants } from './thumbnail.js';
3
5
  import { initialUploadState, uploadReducer } from './upload-state.js';
@@ -120,6 +122,8 @@ export async function runUpload(deps, request, previous) {
120
122
  dispatch({ type: 'transferred' });
121
123
  // --- confirm: metadata record linked to the stored blob --------------------
122
124
  try {
125
+ // EC-568: the ONE embedded-metadata reader, for a first upload and a replace alike.
126
+ const embedded = await embeddedAssetMeta(file, deps.assetFields);
123
127
  const detect = deps.detectDimensions ?? defaultDetectDimensions;
124
128
  const dimensions = await detect(file);
125
129
  // EC-400 + EC-420: produce the thumb + lqip previews from the bytes already in
@@ -134,17 +138,30 @@ export async function runUpload(deps, request, previous) {
134
138
  variants = {};
135
139
  }
136
140
  const url = (await blobs.resolveUrl?.(storageKey)) ?? blobs.url(storageKey);
137
- const asset = await assets.upsertAsset(projectId, {
141
+ const derived = {
138
142
  filename: file.name,
139
143
  contentType: file.type || 'application/octet-stream',
140
144
  byteSize: file.size,
141
145
  storageKey,
142
146
  url,
143
- ...(dimensions ? { width: dimensions.width, height: dimensions.height } : {}),
144
- ...(variants.thumb ? { thumb: variants.thumb } : {}),
145
- ...(variants.lqip ? { lqip: variants.lqip } : {}),
146
- ...(request.alt !== undefined ? { alt: request.alt } : {}),
147
- });
147
+ };
148
+ let write;
149
+ if (request.replace) {
150
+ const stored = await assets.findAsset(projectId, request.replace.assetId);
151
+ if (!stored)
152
+ throw new Error(`No asset “${request.replace.assetId}” is stored to replace the file of.`);
153
+ write = replaceWrite(stored, derived, dimensions, variants, embedded);
154
+ }
155
+ else {
156
+ write = {
157
+ ...derived,
158
+ ...(dimensions ? { width: dimensions.width, height: dimensions.height } : {}),
159
+ ...(variants.thumb ? { thumb: variants.thumb } : {}),
160
+ ...(variants.lqip ? { lqip: variants.lqip } : {}),
161
+ ...(Object.keys(embedded).length > 0 ? { meta: embedded } : {}),
162
+ };
163
+ }
164
+ const asset = await assets.upsertAsset(projectId, write);
148
165
  dispatch({ type: 'confirmed', assetId: asset.id });
149
166
  return { ok: true, asset, state };
150
167
  }
@@ -153,4 +170,42 @@ export async function runUpload(deps, request, previous) {
153
170
  return { ok: false, failure: failureOf('confirm', error), state };
154
171
  }
155
172
  }
173
+ /**
174
+ * The file's embedded facts as `meta` values (EC-568): `readEmbeddedMetadata` over the
175
+ * bytes, mapped through the asset collection's `source` declarations. A file that cannot
176
+ * be read yields `{}` — metadata never fails an upload.
177
+ */
178
+ async function embeddedAssetMeta(file, fields) {
179
+ if (!fields || !fields.some((field) => field.source !== undefined))
180
+ return {};
181
+ try {
182
+ return embeddedMetaValues(fields, readEmbeddedMetadata(new Uint8Array(await file.arrayBuffer())));
183
+ }
184
+ catch {
185
+ return {};
186
+ }
187
+ }
188
+ /**
189
+ * The write of a file REPLACE (EC-568, the owner's ruling: "override the values with
190
+ * available metadata, or keep the existing if no metadata is available"):
191
+ * - the bytes' columns are SET — and a derived column the new file does not have is
192
+ * CLEARED (`null`), so an old thumb, lqip or dimension never outlives its file;
193
+ * - `meta` is the stored bag (a legacy `alt` column lifted in, EC-587) with each key the
194
+ * new file brings overridden; written only when the file brings something, and then
195
+ * the legacy column is cleared, as every `meta` write does;
196
+ * - the id, the focal point, the title and every other column are absent → kept.
197
+ */
198
+ export function replaceWrite(stored, derived, dimensions, variants, embedded) {
199
+ const brings = Object.keys(embedded).length > 0;
200
+ const base = liftLegacyAlt(stored)?.meta ?? stored.meta ?? {};
201
+ return {
202
+ id: stored.id,
203
+ ...derived,
204
+ width: dimensions?.width ?? null,
205
+ height: dimensions?.height ?? null,
206
+ thumb: variants.thumb ?? null,
207
+ lqip: variants.lqip ?? null,
208
+ ...(brings ? { meta: { ...base, ...embedded }, alt: null } : {}),
209
+ };
210
+ }
156
211
  //# sourceMappingURL=upload.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"upload.js","sourceRoot":"","sources":["../../../src/lib/assets/upload.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n AssetRecord,\n AssetRepository,\n BlobStorageAdapter,\n} from '@elytracms/core/persistence'\nimport { detectImageDimensions } from './image-dimensions'\nimport type { ImageDimensions } from './image-dimensions'\nimport { generateVariants } from './thumbnail'\nimport type { GeneratedVariants } from './thumbnail'\nimport { initialUploadState, uploadReducer } from './upload-state'\nimport type { UploadEvent, UploadFailure, UploadState } from './upload-state'\n\n/**\n * Upload orchestration (EC-151): request upload URL → transfer bytes →\n * confirm by writing the EC-044 metadata record (dimensions, mime, serve URL).\n * Storage is reached *only* through the {@link BlobStorageAdapter}; every\n * effect is injectable so the whole flow unit-tests without a browser or a\n * live Convex deployment.\n */\n\n/** The structural slice of a browser `File` the upload flow needs. */\nexport interface UploadableFile {\n name: string\n type: string\n size: number\n arrayBuffer(): Promise<ArrayBuffer>\n}\n\n/** Sends bytes to an upload URL, reporting progress; returns the assigned storage key. */\nexport type UploadTransport = (\n uploadUrl: string,\n file: UploadableFile,\n onProgress: (loaded: number, total: number) => void,\n) => Promise<{ storageKey: string }>\n\n/**\n * Default browser transport: XMLHttpRequest POST for real byte progress\n * (fetch has no upload progress without streams support everywhere).\n */\nexport const xhrUploadTransport: UploadTransport = (uploadUrl, file, onProgress) =>\n new Promise((resolve, reject) => {\n const xhr = new XMLHttpRequest()\n xhr.open('POST', uploadUrl)\n if (file.type) xhr.setRequestHeader('Content-Type', file.type)\n xhr.upload.onprogress = (event) => {\n if (event.lengthComputable) onProgress(event.loaded, event.total)\n }\n xhr.onerror = () => reject(new Error('Network error while uploading bytes.'))\n xhr.onabort = () => reject(new Error('Upload was aborted.'))\n xhr.onload = () => {\n if (xhr.status < 200 || xhr.status >= 300) {\n reject(new Error(`Upload failed with HTTP ${xhr.status}.`))\n return\n }\n try {\n const payload = JSON.parse(xhr.responseText) as { storageId?: unknown }\n if (typeof payload.storageId === 'string' && payload.storageId.length > 0) {\n resolve({ storageKey: payload.storageId })\n } else {\n reject(new Error('Upload response did not include a storageId.'))\n }\n } catch {\n reject(new Error('Upload response was not valid JSON.'))\n }\n }\n void file.arrayBuffer().then(\n (bytes) => xhr.send(bytes),\n () => reject(new Error('Could not read file bytes.')),\n )\n })\n\nexport interface UploadDeps {\n blobs: BlobStorageAdapter\n assets: AssetRepository\n /** Client-direct transport; only used when `blobs.createUploadUrl` exists. */\n transport?: UploadTransport\n /** Injectable dimension detection; defaults to `createImageBitmap`-based. */\n detectDimensions?: (file: UploadableFile) => Promise<ImageDimensions | undefined>\n /**\n * Injectable preview generation (EC-400 thumb + EC-420 lqip). Defaults to a\n * single `createImageBitmap` + canvas decode that stores the thumb via a second\n * `blobs.store()` and inlines the lqip data URI. Returns `{}` (or a partial) for\n * skipped/failed variants — a preview NEVER fails the upload (AD-2). Tests inject\n * a deterministic implementation.\n */\n generateVariants?: (\n file: UploadableFile,\n dimensions: ImageDimensions | undefined,\n ) => Promise<GeneratedVariants>\n}\n\nexport interface UploadRequest {\n projectId: string\n file: UploadableFile\n alt?: string\n /** Observe every state transition (drives progress UI). */\n onState?: (state: UploadState) => void\n}\n\nexport type UploadResult =\n | { ok: true; asset: AssetRecord; state: UploadState }\n | { ok: false; failure: UploadFailure; state: UploadState }\n\nasync function defaultDetectDimensions(file: UploadableFile): Promise<ImageDimensions | undefined> {\n if (typeof Blob === 'undefined' || !(file instanceof Blob)) return undefined\n return detectImageDimensions(file as Blob & { type: string })\n}\n\n/**\n * Default preview step (EC-400 thumb + EC-420 lqip): decode the real `File` (which\n * IS a `Blob` in the browser) ONCE and produce both variants — store the thumb on\n * `blobs`, inline the lqip data URI. Guards the `Blob` narrowing so a non-browser\n * `UploadableFile` (or a backend without `store()`) simply yields no thumb — never\n * an error that could fail the upload.\n */\nfunction defaultGenerateVariants(blobs: BlobStorageAdapter) {\n return async (\n file: UploadableFile,\n dimensions: ImageDimensions | undefined,\n ): Promise<GeneratedVariants> => {\n if (typeof Blob === 'undefined' || !(file instanceof Blob)) return {}\n return generateVariants({ blobs }, file, {\n contentType: file.type,\n ...(dimensions ? { width: dimensions.width, height: dimensions.height } : {}),\n })\n }\n}\n\nfunction failureOf(stage: UploadFailure['stage'], error: unknown): UploadFailure {\n return {\n stage,\n message: error instanceof Error ? error.message : String(error),\n retryable: true,\n }\n}\n\n/**\n * Run one upload attempt. Resolves with a discriminated result — it never\n * throws, and a failed transfer leaves no metadata record behind. Pass the\n * previous `state` to retry (the attempt counter carries over).\n */\nexport async function runUpload(\n deps: UploadDeps,\n request: UploadRequest,\n previous?: UploadState,\n): Promise<UploadResult> {\n let state = previous ?? initialUploadState\n const dispatch = (event: UploadEvent): void => {\n state = uploadReducer(state, event)\n request.onState?.(state)\n }\n dispatch(state.phase === 'failed' ? { type: 'retry' } : { type: 'start' })\n\n const { blobs, assets } = deps\n const { projectId, file } = request\n\n // --- prepare + transfer ---------------------------------------------------\n let storageKey: string\n try {\n if (blobs.createUploadUrl && deps.transport) {\n const uploadUrl = await blobs.createUploadUrl()\n dispatch({ type: 'url-ready' })\n try {\n const sent = await deps.transport(uploadUrl, file, (loaded, total) =>\n dispatch({ type: 'progress', loaded, total }),\n )\n storageKey = sent.storageKey\n } catch (error) {\n dispatch({ type: 'failed', ...failureOf('transfer', error) })\n return { ok: false, failure: failureOf('transfer', error), state }\n }\n } else {\n // Backend without client-direct uploads: push bytes through the adapter.\n dispatch({ type: 'url-ready' })\n const bytes = new Uint8Array(await file.arrayBuffer())\n try {\n if (blobs.store) {\n storageKey = await blobs.store(bytes, file.type)\n } else {\n storageKey = `${projectId}/${file.name}`\n await blobs.put(storageKey, bytes, file.type)\n }\n } catch (error) {\n dispatch({ type: 'failed', ...failureOf('transfer', error) })\n return { ok: false, failure: failureOf('transfer', error), state }\n }\n }\n } catch (error) {\n dispatch({ type: 'failed', ...failureOf('prepare', error) })\n return { ok: false, failure: failureOf('prepare', error), state }\n }\n dispatch({ type: 'transferred' })\n\n // --- confirm: metadata record linked to the stored blob --------------------\n try {\n const detect = deps.detectDimensions ?? defaultDetectDimensions\n const dimensions = await detect(file)\n // EC-400 + EC-420: produce the thumb + lqip previews from the bytes already in\n // hand (one decode). A preview failure is swallowed — the asset simply ships\n // without them (AD-2, scope §4); absence = today's behaviour, never an error.\n const generate = deps.generateVariants ?? defaultGenerateVariants(blobs)\n let variants: GeneratedVariants\n try {\n variants = await generate(file, dimensions)\n } catch {\n variants = {}\n }\n const url = (await blobs.resolveUrl?.(storageKey)) ?? blobs.url(storageKey)\n const asset = await assets.upsertAsset(projectId, {\n filename: file.name,\n contentType: file.type || 'application/octet-stream',\n byteSize: file.size,\n storageKey,\n url,\n ...(dimensions ? { width: dimensions.width, height: dimensions.height } : {}),\n ...(variants.thumb ? { thumb: variants.thumb } : {}),\n ...(variants.lqip ? { lqip: variants.lqip } : {}),\n ...(request.alt !== undefined ? { alt: request.alt } : {}),\n })\n dispatch({ type: 'confirmed', assetId: asset.id })\n return { ok: true, asset, state }\n } catch (error) {\n dispatch({ type: 'failed', ...failureOf('confirm', error) })\n return { ok: false, failure: failureOf('confirm', error), state }\n }\n}\n"]}
1
+ {"version":3,"file":"upload.js","sourceRoot":"","sources":["../../../src/lib/assets/upload.ts"],"names":[],"mappings":"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","sourcesContent":["import { embeddedMetaValues, readEmbeddedMetadata } from '@elytracms/core/cms-core'\nimport type { FieldDef } from '@elytracms/core/cms-core'\nimport { liftLegacyAlt } from '@elytracms/core/persistence'\nimport type {\n AssetRecord,\n AssetRepository,\n AssetWriteInput,\n BlobStorageAdapter,\n} from '@elytracms/core/persistence'\nimport { detectImageDimensions } from './image-dimensions'\nimport type { ImageDimensions } from './image-dimensions'\nimport { generateVariants } from './thumbnail'\nimport type { GeneratedVariants } from './thumbnail'\nimport { initialUploadState, uploadReducer } from './upload-state'\nimport type { UploadEvent, UploadFailure, UploadState } from './upload-state'\n\n/**\n * Upload orchestration (EC-151): request upload URL → transfer bytes →\n * confirm by writing the EC-044 metadata record (dimensions, mime, serve URL).\n * Storage is reached *only* through the {@link BlobStorageAdapter}; every\n * effect is injectable so the whole flow unit-tests without a browser or a\n * live Convex deployment.\n */\n\n/** The structural slice of a browser `File` the upload flow needs. */\nexport interface UploadableFile {\n name: string\n type: string\n size: number\n arrayBuffer(): Promise<ArrayBuffer>\n}\n\n/** Sends bytes to an upload URL, reporting progress; returns the assigned storage key. */\nexport type UploadTransport = (\n uploadUrl: string,\n file: UploadableFile,\n onProgress: (loaded: number, total: number) => void,\n) => Promise<{ storageKey: string }>\n\n/**\n * Default browser transport: XMLHttpRequest POST for real byte progress\n * (fetch has no upload progress without streams support everywhere).\n */\nexport const xhrUploadTransport: UploadTransport = (uploadUrl, file, onProgress) =>\n new Promise((resolve, reject) => {\n const xhr = new XMLHttpRequest()\n xhr.open('POST', uploadUrl)\n if (file.type) xhr.setRequestHeader('Content-Type', file.type)\n xhr.upload.onprogress = (event) => {\n if (event.lengthComputable) onProgress(event.loaded, event.total)\n }\n xhr.onerror = () => reject(new Error('Network error while uploading bytes.'))\n xhr.onabort = () => reject(new Error('Upload was aborted.'))\n xhr.onload = () => {\n if (xhr.status < 200 || xhr.status >= 300) {\n reject(new Error(`Upload failed with HTTP ${xhr.status}.`))\n return\n }\n try {\n const payload = JSON.parse(xhr.responseText) as { storageId?: unknown }\n if (typeof payload.storageId === 'string' && payload.storageId.length > 0) {\n resolve({ storageKey: payload.storageId })\n } else {\n reject(new Error('Upload response did not include a storageId.'))\n }\n } catch {\n reject(new Error('Upload response was not valid JSON.'))\n }\n }\n void file.arrayBuffer().then(\n (bytes) => xhr.send(bytes),\n () => reject(new Error('Could not read file bytes.')),\n )\n })\n\nexport interface UploadDeps {\n blobs: BlobStorageAdapter\n assets: AssetRepository\n /**\n * The project's asset collection fields (EC-568): the `source` declarations that map\n * the file's embedded IPTC facts onto `meta` keys. Absent → no embedded metadata is\n * written (a field without `source` is never filled).\n */\n assetFields?: readonly FieldDef[]\n /** Client-direct transport; only used when `blobs.createUploadUrl` exists. */\n transport?: UploadTransport\n /** Injectable dimension detection; defaults to `createImageBitmap`-based. */\n detectDimensions?: (file: UploadableFile) => Promise<ImageDimensions | undefined>\n /**\n * Injectable preview generation (EC-400 thumb + EC-420 lqip). Defaults to a\n * single `createImageBitmap` + canvas decode that stores the thumb via a second\n * `blobs.store()` and inlines the lqip data URI. Returns `{}` (or a partial) for\n * skipped/failed variants — a preview NEVER fails the upload (AD-2). Tests inject\n * a deterministic implementation.\n */\n generateVariants?: (\n file: UploadableFile,\n dimensions: ImageDimensions | undefined,\n ) => Promise<GeneratedVariants>\n}\n\nexport interface UploadRequest {\n projectId: string\n file: UploadableFile\n /**\n * EC-568 — REPLACE the file inside this existing asset instead of creating one: the\n * same id, the new bytes and everything derived from them (storage key, url,\n * dimensions, type, size, thumb, lqip) through this one upload path; `meta` overridden\n * per key where the new file carries a value (else kept); the focal point, the title\n * and every other key kept. The previous blobs are kept (see EC-568 § Done).\n */\n replace?: { assetId: string }\n /** Observe every state transition (drives progress UI). */\n onState?: (state: UploadState) => void\n}\n\nexport type UploadResult =\n | { ok: true; asset: AssetRecord; state: UploadState }\n | { ok: false; failure: UploadFailure; state: UploadState }\n\nasync function defaultDetectDimensions(file: UploadableFile): Promise<ImageDimensions | undefined> {\n if (typeof Blob === 'undefined' || !(file instanceof Blob)) return undefined\n return detectImageDimensions(file as Blob & { type: string })\n}\n\n/**\n * Default preview step (EC-400 thumb + EC-420 lqip): decode the real `File` (which\n * IS a `Blob` in the browser) ONCE and produce both variants — store the thumb on\n * `blobs`, inline the lqip data URI. Guards the `Blob` narrowing so a non-browser\n * `UploadableFile` (or a backend without `store()`) simply yields no thumb — never\n * an error that could fail the upload.\n */\nfunction defaultGenerateVariants(blobs: BlobStorageAdapter) {\n return async (\n file: UploadableFile,\n dimensions: ImageDimensions | undefined,\n ): Promise<GeneratedVariants> => {\n if (typeof Blob === 'undefined' || !(file instanceof Blob)) return {}\n return generateVariants({ blobs }, file, {\n contentType: file.type,\n ...(dimensions ? { width: dimensions.width, height: dimensions.height } : {}),\n })\n }\n}\n\nfunction failureOf(stage: UploadFailure['stage'], error: unknown): UploadFailure {\n return {\n stage,\n message: error instanceof Error ? error.message : String(error),\n retryable: true,\n }\n}\n\n/**\n * Run one upload attempt. Resolves with a discriminated result — it never\n * throws, and a failed transfer leaves no metadata record behind. Pass the\n * previous `state` to retry (the attempt counter carries over).\n */\nexport async function runUpload(\n deps: UploadDeps,\n request: UploadRequest,\n previous?: UploadState,\n): Promise<UploadResult> {\n let state = previous ?? initialUploadState\n const dispatch = (event: UploadEvent): void => {\n state = uploadReducer(state, event)\n request.onState?.(state)\n }\n dispatch(state.phase === 'failed' ? { type: 'retry' } : { type: 'start' })\n\n const { blobs, assets } = deps\n const { projectId, file } = request\n\n // --- prepare + transfer ---------------------------------------------------\n let storageKey: string\n try {\n if (blobs.createUploadUrl && deps.transport) {\n const uploadUrl = await blobs.createUploadUrl()\n dispatch({ type: 'url-ready' })\n try {\n const sent = await deps.transport(uploadUrl, file, (loaded, total) =>\n dispatch({ type: 'progress', loaded, total }),\n )\n storageKey = sent.storageKey\n } catch (error) {\n dispatch({ type: 'failed', ...failureOf('transfer', error) })\n return { ok: false, failure: failureOf('transfer', error), state }\n }\n } else {\n // Backend without client-direct uploads: push bytes through the adapter.\n dispatch({ type: 'url-ready' })\n const bytes = new Uint8Array(await file.arrayBuffer())\n try {\n if (blobs.store) {\n storageKey = await blobs.store(bytes, file.type)\n } else {\n storageKey = `${projectId}/${file.name}`\n await blobs.put(storageKey, bytes, file.type)\n }\n } catch (error) {\n dispatch({ type: 'failed', ...failureOf('transfer', error) })\n return { ok: false, failure: failureOf('transfer', error), state }\n }\n }\n } catch (error) {\n dispatch({ type: 'failed', ...failureOf('prepare', error) })\n return { ok: false, failure: failureOf('prepare', error), state }\n }\n dispatch({ type: 'transferred' })\n\n // --- confirm: metadata record linked to the stored blob --------------------\n try {\n // EC-568: the ONE embedded-metadata reader, for a first upload and a replace alike.\n const embedded = await embeddedAssetMeta(file, deps.assetFields)\n const detect = deps.detectDimensions ?? defaultDetectDimensions\n const dimensions = await detect(file)\n // EC-400 + EC-420: produce the thumb + lqip previews from the bytes already in\n // hand (one decode). A preview failure is swallowed — the asset simply ships\n // without them (AD-2, scope §4); absence = today's behaviour, never an error.\n const generate = deps.generateVariants ?? defaultGenerateVariants(blobs)\n let variants: GeneratedVariants\n try {\n variants = await generate(file, dimensions)\n } catch {\n variants = {}\n }\n const url = (await blobs.resolveUrl?.(storageKey)) ?? blobs.url(storageKey)\n const derived = {\n filename: file.name,\n contentType: file.type || 'application/octet-stream',\n byteSize: file.size,\n storageKey,\n url,\n }\n let write: AssetWriteInput\n if (request.replace) {\n const stored = await assets.findAsset(projectId, request.replace.assetId)\n if (!stored) throw new Error(`No asset “${request.replace.assetId}” is stored to replace the file of.`)\n write = replaceWrite(stored, derived, dimensions, variants, embedded)\n } else {\n write = {\n ...derived,\n ...(dimensions ? { width: dimensions.width, height: dimensions.height } : {}),\n ...(variants.thumb ? { thumb: variants.thumb } : {}),\n ...(variants.lqip ? { lqip: variants.lqip } : {}),\n ...(Object.keys(embedded).length > 0 ? { meta: embedded } : {}),\n }\n }\n const asset = await assets.upsertAsset(projectId, write)\n dispatch({ type: 'confirmed', assetId: asset.id })\n return { ok: true, asset, state }\n } catch (error) {\n dispatch({ type: 'failed', ...failureOf('confirm', error) })\n return { ok: false, failure: failureOf('confirm', error), state }\n }\n}\n\n/**\n * The file's embedded facts as `meta` values (EC-568): `readEmbeddedMetadata` over the\n * bytes, mapped through the asset collection's `source` declarations. A file that cannot\n * be read yields `{}` — metadata never fails an upload.\n */\nasync function embeddedAssetMeta(\n file: UploadableFile,\n fields: readonly FieldDef[] | undefined,\n): Promise<Record<string, unknown>> {\n if (!fields || !fields.some((field) => field.source !== undefined)) return {}\n try {\n return embeddedMetaValues(fields, readEmbeddedMetadata(new Uint8Array(await file.arrayBuffer())))\n } catch {\n return {}\n }\n}\n\n/**\n * The write of a file REPLACE (EC-568, the owner's ruling: \"override the values with\n * available metadata, or keep the existing if no metadata is available\"):\n * - the bytes' columns are SET — and a derived column the new file does not have is\n * CLEARED (`null`), so an old thumb, lqip or dimension never outlives its file;\n * - `meta` is the stored bag (a legacy `alt` column lifted in, EC-587) with each key the\n * new file brings overridden; written only when the file brings something, and then\n * the legacy column is cleared, as every `meta` write does;\n * - the id, the focal point, the title and every other column are absent → kept.\n */\nexport function replaceWrite(\n stored: AssetRecord,\n derived: Pick<AssetRecord, 'filename' | 'contentType' | 'byteSize' | 'storageKey'> & { url: string },\n dimensions: ImageDimensions | undefined,\n variants: GeneratedVariants,\n embedded: Record<string, unknown>,\n): AssetWriteInput {\n const brings = Object.keys(embedded).length > 0\n const base = liftLegacyAlt(stored)?.meta ?? stored.meta ?? {}\n return {\n id: stored.id,\n ...derived,\n width: dimensions?.width ?? null,\n height: dimensions?.height ?? null,\n thumb: variants.thumb ?? null,\n lqip: variants.lqip ?? null,\n ...(brings ? { meta: { ...base, ...embedded }, alt: null } : {}),\n }\n}\n"]}
@@ -26,8 +26,16 @@ interface DataTransferReader {
26
26
  }
27
27
  /** Write all three EC-174 representations onto a clipboard event's DataTransfer. */
28
28
  export declare function writeFragmentToDataTransfer(dt: DataTransferWriter, fragment: ClipboardFragment): void;
29
+ /**
30
+ * What a paste read yields: the fragment parse, plus — when the clipboard holds no
31
+ * fragment — its plain text (EC-549: the canvas turns plain text into a `text`-role
32
+ * component node, never into loose prose).
33
+ */
34
+ export type ClipboardRead = ParseFragmentResult & {
35
+ plainText?: string;
36
+ };
29
37
  /** Read a fragment from a paste event's DataTransfer, in fidelity order. */
30
- export declare function readFragmentFromDataTransfer(dt: DataTransferReader): ParseFragmentResult;
38
+ export declare function readFragmentFromDataTransfer(dt: DataTransferReader): ClipboardRead;
31
39
  /** The async-clipboard surface we use (subset of `Navigator['clipboard']`). */
32
40
  export interface AsyncClipboard {
33
41
  write?(items: ClipboardItem[]): Promise<void>;
@@ -42,5 +50,5 @@ export interface AsyncClipboard {
42
50
  */
43
51
  export declare function writeFragmentToClipboard(clipboard: AsyncClipboard | undefined, fragment: ClipboardFragment): Promise<void>;
44
52
  /** Read a fragment via the async Clipboard API (`read`, falling back to `readText`). */
45
- export declare function readFragmentFromClipboard(clipboard: AsyncClipboard | undefined): Promise<ParseFragmentResult>;
53
+ export declare function readFragmentFromClipboard(clipboard: AsyncClipboard | undefined): Promise<ClipboardRead>;
46
54
  export {};
@@ -25,16 +25,20 @@ export function writeFragmentToDataTransfer(dt, fragment) {
25
25
  dt.setData('text/html', reps.html);
26
26
  dt.setData(DATA_TRANSFER_JSON_TYPE, reps.json);
27
27
  }
28
+ /** Attach the plain text to a failed parse (a fragment always wins). */
29
+ function withPlainText(result, text) {
30
+ return !result.ok && text !== undefined && text.trim() !== '' ? { ...result, plainText: text } : result;
31
+ }
28
32
  /** Read a fragment from a paste event's DataTransfer, in fidelity order. */
29
33
  export function readFragmentFromDataTransfer(dt) {
30
34
  const json = dt.getData(DATA_TRANSFER_JSON_TYPE);
31
35
  const html = dt.getData('text/html');
32
36
  const text = dt.getData('text/plain');
33
- return parseFragmentFromClipboard({
37
+ return withPlainText(parseFragmentFromClipboard({
34
38
  json: json || undefined,
35
39
  html: html || undefined,
36
40
  text: text || undefined,
37
- });
41
+ }), text || undefined);
38
42
  }
39
43
  /**
40
44
  * Write a fragment via the async Clipboard API. Tries the multi-representation
@@ -78,7 +82,7 @@ export async function readFragmentFromClipboard(clipboard) {
78
82
  text = await (await item.getType('text/plain')).text();
79
83
  }
80
84
  if (html !== undefined || text !== undefined) {
81
- return parseFragmentFromClipboard({ html, text });
85
+ return withPlainText(parseFragmentFromClipboard({ html, text }), text);
82
86
  }
83
87
  }
84
88
  catch {
@@ -88,7 +92,7 @@ export async function readFragmentFromClipboard(clipboard) {
88
92
  if (clipboard?.readText) {
89
93
  try {
90
94
  const text = await clipboard.readText();
91
- return parseFragmentFromClipboard({ text });
95
+ return withPlainText(parseFragmentFromClipboard({ text }), text);
92
96
  }
93
97
  catch {
94
98
  // fall through to the structured error below
@@ -1 +1 @@
1
- {"version":3,"file":"os-clipboard.js","sourceRoot":"","sources":["../../../src/lib/clipboard/os-clipboard.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n parseFragmentFromClipboard,\n toClipboardRepresentations,\n} from '@elytracms/core/clipboard'\nimport type { ClipboardFragment, ParseFragmentResult } from '@elytracms/core/clipboard'\n\n/**\n * OS-clipboard transport (EC-175). The studio NEVER keeps an app-internal\n * copy buffer: every copy writes the EC-174 fragment representations to the\n * operating-system clipboard, and every paste reads them back — which is what\n * makes copy/paste work across documents, pages, projects, and browser tabs\n * for free.\n *\n * Two transports, by trigger:\n * - **ClipboardEvent / DataTransfer** (synchronous, no permissions): native\n * copy/cut/paste events expose `clipboardData` and accept arbitrary types,\n * so the canonical JSON travels as `application/json` alongside\n * `text/plain` + `text/html`.\n * - **navigator.clipboard** (async, used by explicit button affordances):\n * `write()` with a `ClipboardItem` carrying `text/html` + `text/plain`\n * (the JSON rides in `text/plain` and embedded in the HTML), falling back\n * to `writeText(json)` where `write`/`ClipboardItem` are unavailable.\n */\n\n/** The DataTransfer type carrying the canonical fragment JSON. */\nexport const DATA_TRANSFER_JSON_TYPE = 'application/json'\n\ninterface DataTransferWriter {\n setData(type: string, data: string): void\n}\n\ninterface DataTransferReader {\n getData(type: string): string\n}\n\n/** Write all three EC-174 representations onto a clipboard event's DataTransfer. */\nexport function writeFragmentToDataTransfer(dt: DataTransferWriter, fragment: ClipboardFragment): void {\n const reps = toClipboardRepresentations(fragment)\n dt.setData('text/plain', reps.text)\n dt.setData('text/html', reps.html)\n dt.setData(DATA_TRANSFER_JSON_TYPE, reps.json)\n}\n\n/** Read a fragment from a paste event's DataTransfer, in fidelity order. */\nexport function readFragmentFromDataTransfer(dt: DataTransferReader): ParseFragmentResult {\n const json = dt.getData(DATA_TRANSFER_JSON_TYPE)\n const html = dt.getData('text/html')\n const text = dt.getData('text/plain')\n return parseFragmentFromClipboard({\n json: json || undefined,\n html: html || undefined,\n text: text || undefined,\n })\n}\n\n/** The async-clipboard surface we use (subset of `Navigator['clipboard']`). */\nexport interface AsyncClipboard {\n write?(items: ClipboardItem[]): Promise<void>\n writeText?(text: string): Promise<void>\n read?(): Promise<ClipboardItems>\n readText?(): Promise<string>\n}\n\n/**\n * Write a fragment via the async Clipboard API. Tries the multi-representation\n * `ClipboardItem` first; falls back to `writeText` with the canonical JSON\n * (which `parseFragmentFromClipboard` restores from `text/plain`).\n */\nexport async function writeFragmentToClipboard(\n clipboard: AsyncClipboard | undefined,\n fragment: ClipboardFragment,\n): Promise<void> {\n if (!clipboard) throw new Error('Clipboard API is unavailable.')\n const reps = toClipboardRepresentations(fragment)\n const ClipboardItemCtor = (globalThis as { ClipboardItem?: typeof ClipboardItem }).ClipboardItem\n if (clipboard.write && ClipboardItemCtor) {\n try {\n await clipboard.write([\n new ClipboardItemCtor({\n 'text/plain': new Blob([reps.text], { type: 'text/plain' }),\n 'text/html': new Blob([reps.html], { type: 'text/html' }),\n }),\n ])\n return\n } catch {\n // fall through to writeText\n }\n }\n if (!clipboard.writeText) throw new Error('Clipboard API is unavailable.')\n await clipboard.writeText(reps.json)\n}\n\n/** Read a fragment via the async Clipboard API (`read`, falling back to `readText`). */\nexport async function readFragmentFromClipboard(\n clipboard: AsyncClipboard | undefined,\n): Promise<ParseFragmentResult> {\n if (clipboard?.read) {\n try {\n const items = await clipboard.read()\n let html: string | undefined\n let text: string | undefined\n for (const item of items) {\n if (item.types.includes('text/html')) html = await (await item.getType('text/html')).text()\n if (item.types.includes('text/plain')) text = await (await item.getType('text/plain')).text()\n }\n if (html !== undefined || text !== undefined) {\n return parseFragmentFromClipboard({ html, text })\n }\n } catch {\n // fall through to readText\n }\n }\n if (clipboard?.readText) {\n try {\n const text = await clipboard.readText()\n return parseFragmentFromClipboard({ text })\n } catch {\n // fall through to the structured error below\n }\n }\n return {\n ok: false,\n error: {\n code: 'foreign-format',\n message: 'The clipboard could not be read (no permission or unsupported browser).',\n },\n }\n}\n"]}
1
+ {"version":3,"file":"os-clipboard.js","sourceRoot":"","sources":["../../../src/lib/clipboard/os-clipboard.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n parseFragmentFromClipboard,\n toClipboardRepresentations,\n} from '@elytracms/core/clipboard'\nimport type { ClipboardFragment, ParseFragmentResult } from '@elytracms/core/clipboard'\n\n/**\n * OS-clipboard transport (EC-175). The studio NEVER keeps an app-internal\n * copy buffer: every copy writes the EC-174 fragment representations to the\n * operating-system clipboard, and every paste reads them back — which is what\n * makes copy/paste work across documents, pages, projects, and browser tabs\n * for free.\n *\n * Two transports, by trigger:\n * - **ClipboardEvent / DataTransfer** (synchronous, no permissions): native\n * copy/cut/paste events expose `clipboardData` and accept arbitrary types,\n * so the canonical JSON travels as `application/json` alongside\n * `text/plain` + `text/html`.\n * - **navigator.clipboard** (async, used by explicit button affordances):\n * `write()` with a `ClipboardItem` carrying `text/html` + `text/plain`\n * (the JSON rides in `text/plain` and embedded in the HTML), falling back\n * to `writeText(json)` where `write`/`ClipboardItem` are unavailable.\n */\n\n/** The DataTransfer type carrying the canonical fragment JSON. */\nexport const DATA_TRANSFER_JSON_TYPE = 'application/json'\n\ninterface DataTransferWriter {\n setData(type: string, data: string): void\n}\n\ninterface DataTransferReader {\n getData(type: string): string\n}\n\n/** Write all three EC-174 representations onto a clipboard event's DataTransfer. */\nexport function writeFragmentToDataTransfer(dt: DataTransferWriter, fragment: ClipboardFragment): void {\n const reps = toClipboardRepresentations(fragment)\n dt.setData('text/plain', reps.text)\n dt.setData('text/html', reps.html)\n dt.setData(DATA_TRANSFER_JSON_TYPE, reps.json)\n}\n\n/**\n * What a paste read yields: the fragment parse, plus — when the clipboard holds no\n * fragment — its plain text (EC-549: the canvas turns plain text into a `text`-role\n * component node, never into loose prose).\n */\nexport type ClipboardRead = ParseFragmentResult & { plainText?: string }\n\n/** Attach the plain text to a failed parse (a fragment always wins). */\nfunction withPlainText(result: ParseFragmentResult, text: string | undefined): ClipboardRead {\n return !result.ok && text !== undefined && text.trim() !== '' ? { ...result, plainText: text } : result\n}\n\n/** Read a fragment from a paste event's DataTransfer, in fidelity order. */\nexport function readFragmentFromDataTransfer(dt: DataTransferReader): ClipboardRead {\n const json = dt.getData(DATA_TRANSFER_JSON_TYPE)\n const html = dt.getData('text/html')\n const text = dt.getData('text/plain')\n return withPlainText(\n parseFragmentFromClipboard({\n json: json || undefined,\n html: html || undefined,\n text: text || undefined,\n }),\n text || undefined,\n )\n}\n\n/** The async-clipboard surface we use (subset of `Navigator['clipboard']`). */\nexport interface AsyncClipboard {\n write?(items: ClipboardItem[]): Promise<void>\n writeText?(text: string): Promise<void>\n read?(): Promise<ClipboardItems>\n readText?(): Promise<string>\n}\n\n/**\n * Write a fragment via the async Clipboard API. Tries the multi-representation\n * `ClipboardItem` first; falls back to `writeText` with the canonical JSON\n * (which `parseFragmentFromClipboard` restores from `text/plain`).\n */\nexport async function writeFragmentToClipboard(\n clipboard: AsyncClipboard | undefined,\n fragment: ClipboardFragment,\n): Promise<void> {\n if (!clipboard) throw new Error('Clipboard API is unavailable.')\n const reps = toClipboardRepresentations(fragment)\n const ClipboardItemCtor = (globalThis as { ClipboardItem?: typeof ClipboardItem }).ClipboardItem\n if (clipboard.write && ClipboardItemCtor) {\n try {\n await clipboard.write([\n new ClipboardItemCtor({\n 'text/plain': new Blob([reps.text], { type: 'text/plain' }),\n 'text/html': new Blob([reps.html], { type: 'text/html' }),\n }),\n ])\n return\n } catch {\n // fall through to writeText\n }\n }\n if (!clipboard.writeText) throw new Error('Clipboard API is unavailable.')\n await clipboard.writeText(reps.json)\n}\n\n/** Read a fragment via the async Clipboard API (`read`, falling back to `readText`). */\nexport async function readFragmentFromClipboard(\n clipboard: AsyncClipboard | undefined,\n): Promise<ClipboardRead> {\n if (clipboard?.read) {\n try {\n const items = await clipboard.read()\n let html: string | undefined\n let text: string | undefined\n for (const item of items) {\n if (item.types.includes('text/html')) html = await (await item.getType('text/html')).text()\n if (item.types.includes('text/plain')) text = await (await item.getType('text/plain')).text()\n }\n if (html !== undefined || text !== undefined) {\n return withPlainText(parseFragmentFromClipboard({ html, text }), text)\n }\n } catch {\n // fall through to readText\n }\n }\n if (clipboard?.readText) {\n try {\n const text = await clipboard.readText()\n return withPlainText(parseFragmentFromClipboard({ text }), text)\n } catch {\n // fall through to the structured error below\n }\n }\n return {\n ok: false,\n error: {\n code: 'foreign-format',\n message: 'The clipboard could not be read (no permission or unsupported browser).',\n },\n }\n}\n"]}
@@ -1,4 +1,4 @@
1
- import type { ComponentNode, InsertTarget } from '@elytracms/core/project-graph';
1
+ import type { ComponentNode, InsertTarget, PlacementScope } from '@elytracms/core/project-graph';
2
2
  import type { ComponentManifest } from '@elytracms/core/component-registry';
3
3
  /**
4
4
  * Paste landing-spot resolution (EC-175) — the same placement rule the Insert
@@ -15,10 +15,12 @@ import type { ComponentManifest } from '@elytracms/core/component-registry';
15
15
  * validity of the pasted subtree stays with the publish gate, exactly like a
16
16
  * drop.
17
17
  *
18
- * EC-554: the composition FIELD's top-level `allow` is checked here too, through
19
- * the same `placementRefusal` → `fieldAllowAt` the drop rule and the insert offer
20
- * read — a paste that lands as a direct child of the composition root must be in
21
- * the field's vocabulary.
18
+ * EC-554: the composition FIELD's top-level `allow` is checked here too — a paste
19
+ * that lands as a direct child of the composition root must be in the field's
20
+ * vocabulary. EC-584: both through `placementRefusal`, a reader of the one core
21
+ * placement judge the builder reducer enforces (canvas eligibility included — a
22
+ * server-only root is refused here, COMPOSITION-2); the reducer judges the whole
23
+ * pasted subtree again when it lands.
22
24
  */
23
25
  export interface PasteTarget {
24
26
  ok: true;
@@ -49,7 +51,13 @@ export interface PasteFieldRejection {
49
51
  /** The pasted root the field vocabulary refuses. */
50
52
  componentId: string;
51
53
  }
52
- export type PasteRejection = PasteSlotRejection | PasteFieldRejection;
54
+ /** The pasted root is server-only (`clientRenderable: false`) — not canvas-eligible (EC-584). */
55
+ export interface PasteServerOnlyRejection {
56
+ ok: false;
57
+ code: 'server-only';
58
+ componentId: string;
59
+ }
60
+ export type PasteRejection = PasteSlotRejection | PasteFieldRejection | PasteServerOnlyRejection;
53
61
  export type PasteResolution = PasteTarget | PasteRejection;
54
62
  export declare function resolvePasteTarget(args: {
55
63
  root: ComponentNode;
@@ -59,8 +67,9 @@ export declare function resolvePasteTarget(args: {
59
67
  componentId: string;
60
68
  getManifest: (componentId: string) => ComponentManifest | undefined;
61
69
  /**
62
- * The composition field's top-level `allow` (EC-554) — `undefined` when the
63
- * field declares none. Required so no caller can forget to pass it.
70
+ * EC-584: what the placement judge reads — the manifest lookup and the composition
71
+ * field's effective top-level vocabulary (the builder store's `placement`).
72
+ * Required so no caller can forget it.
64
73
  */
65
- fieldAllow: readonly string[] | undefined;
74
+ placement: PlacementScope;
66
75
  }): PasteResolution;
@@ -2,11 +2,13 @@ import { getNode, getParentInfo } from '@elytracms/core/project-graph';
2
2
  import { manifestRules, placementRefusal, primaryInsertSlot } from '@elytracms/studio/lib/dnd/drop-rules';
3
3
  import { componentDisplayName } from '@elytracms/studio/lib/persona/labels';
4
4
  export function resolvePasteTarget(args) {
5
- const { root, componentId, getManifest, fieldAllow } = args;
5
+ const { root, componentId, getManifest, placement } = args;
6
6
  const anchor = args.anchorId !== null ? getNode(root, args.anchorId) : undefined;
7
7
  const node = anchor ?? root;
8
- const leashed = (slot, slotRule, target, destinationLabel) => {
9
- const refusal = placementRefusal({ root, parentId: target.parentId, slot: slotRule, fieldAllow, componentId });
8
+ const leashed = (slot, target, destinationLabel) => {
9
+ const refusal = placementRefusal({ root, parentId: target.parentId, slot, placement, componentId });
10
+ if (refusal === 'server-only')
11
+ return { ok: false, code: 'server-only', componentId };
10
12
  if (refusal === 'field')
11
13
  return { ok: false, code: 'field-not-allowed', componentId };
12
14
  if (refusal === 'slot')
@@ -19,19 +21,18 @@ export function resolvePasteTarget(args) {
19
21
  const manifest = getManifest(node.componentId);
20
22
  const childSlot = primaryInsertSlot(manifestRules(manifest));
21
23
  if (childSlot) {
22
- return leashed(childSlot.name, childSlot, { parentId: node.id, slot: childSlot.name }, `${componentDisplayName(node.componentId, manifest)} · ${childSlot.name}`);
24
+ return leashed(childSlot.name, { parentId: node.id, slot: childSlot.name }, `${componentDisplayName(node.componentId, manifest)} · ${childSlot.name}`);
23
25
  }
24
26
  const info = getParentInfo(root, node.id);
25
27
  if (info) {
26
28
  // A sibling paste is governed by the PARENT's slot leash (the drop rule's
27
29
  // before/after case). An unknown parent manifest imposes no allow-list;
28
30
  // validation owns that issue.
29
- const parentSlot = manifestRules(getManifest(info.parent.componentId))?.slots.find((s) => s.name === info.slot);
30
- return leashed(info.slot, parentSlot, { parentId: info.parent.id, slot: info.slot, index: info.index + 1 }, `${componentDisplayName(info.parent.componentId, getManifest(info.parent.componentId))} · ${info.slot}`);
31
+ return leashed(info.slot, { parentId: info.parent.id, slot: info.slot, index: info.index + 1 }, `${componentDisplayName(info.parent.componentId, getManifest(info.parent.componentId))} · ${info.slot}`);
31
32
  }
32
33
  // Leaf root with no declared slots (degenerate graphs): append to `children`.
33
34
  // No slot rule exists, but the paste still lands at the root — the field leash
34
35
  // applies.
35
- return leashed('children', undefined, { parentId: root.id, slot: 'children' }, 'root');
36
+ return leashed('children', { parentId: root.id, slot: 'children' }, 'root');
36
37
  }
37
38
  //# sourceMappingURL=paste-target.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"paste-target.js","sourceRoot":"","sources":["../../../src/lib/clipboard/paste-target.ts"],"names":[],"mappings":"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","sourcesContent":["import { getNode, getParentInfo } from '@elytracms/core/project-graph'\nimport type { ComponentNode, InsertTarget } from '@elytracms/core/project-graph'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { manifestRules, placementRefusal, primaryInsertSlot } from '@elytracms/studio/lib/dnd/drop-rules'\nimport type { SlotRule } from '@elytracms/studio/lib/dnd/drop-rules'\nimport { componentDisplayName } from '@elytracms/studio/lib/persona/labels'\n\n/**\n * Paste landing-spot resolution (EC-175) — the same placement rule the Insert\n * dialog uses (`insert-context.ts`): a pasted subtree lands *into*\n * the anchor node's child slot when it has one, else *after* the anchor as a\n * sibling, else appended to the page root. Pure and selection-driven so the\n * canvas, the page tree, and keyboard paste all share one rule.\n *\n * EC-545: the resolved slot's `allow` leash is checked HERE, for the pasted\n * subtree's root, through the same `slotAllows` the drop rule reads — so a\n * component drag-and-drop refuses is refused by clipboard too, and a caller\n * cannot reach an `InsertTarget` without passing the check (the rejection is a\n * separate member of the result union, not a flag beside the target). Nested\n * validity of the pasted subtree stays with the publish gate, exactly like a\n * drop.\n *\n * EC-554: the composition FIELD's top-level `allow` is checked here too, through\n * the same `placementRefusal` → `fieldAllowAt` the drop rule and the insert offer\n * read — a paste that lands as a direct child of the composition root must be in\n * the field's vocabulary.\n */\n\nexport interface PasteTarget {\n ok: true\n target: InsertTarget\n /** Human label of where the paste lands (notices/debugging). */\n destinationLabel: string\n}\n\n/** The resolved slot's own `allow` list excludes the pasted root (EC-545). */\nexport interface PasteSlotRejection {\n ok: false\n code: 'not-allowed'\n /** The pasted root the slot refuses. */\n componentId: string\n /** The resolved slot whose `allow` list excludes it. */\n slot: string\n /** Human label of the refused destination (`Section · body`). */\n destinationLabel: string\n}\n\n/**\n * The composition field's top-level `allow` excludes the pasted root (EC-554).\n * Deliberately carries NO slot/destination label: the refused slot is the\n * invisible synthetic root's (`Stack · children`), and the reason to show is the\n * FIELD — the notice names it from the editor's field scope.\n */\nexport interface PasteFieldRejection {\n ok: false\n code: 'field-not-allowed'\n /** The pasted root the field vocabulary refuses. */\n componentId: string\n}\n\nexport type PasteRejection = PasteSlotRejection | PasteFieldRejection\n\nexport type PasteResolution = PasteTarget | PasteRejection\n\nexport function resolvePasteTarget(args: {\n root: ComponentNode\n /** The anchor node (usually the selection); `null` falls back to the root. */\n anchorId: string | null\n /** The pasted subtree's root component — the id the slot leash judges. */\n componentId: string\n getManifest: (componentId: string) => ComponentManifest | undefined\n /**\n * The composition field's top-level `allow` (EC-554) — `undefined` when the\n * field declares none. Required so no caller can forget to pass it.\n */\n fieldAllow: readonly string[] | undefined\n}): PasteResolution {\n const { root, componentId, getManifest, fieldAllow } = args\n const anchor = args.anchorId !== null ? getNode(root, args.anchorId) : undefined\n const node = anchor ?? root\n\n const leashed = (\n slot: string,\n slotRule: SlotRule | undefined,\n target: InsertTarget,\n destinationLabel: string,\n ): PasteResolution => {\n const refusal = placementRefusal({ root, parentId: target.parentId, slot: slotRule, fieldAllow, componentId })\n if (refusal === 'field') return { ok: false, code: 'field-not-allowed', componentId }\n if (refusal === 'slot') return { ok: false, code: 'not-allowed', componentId, slot, destinationLabel }\n return { ok: true, target, destinationLabel }\n }\n\n // EC-386: the shared slot rule (`primaryInsertSlot` over the merged\n // `manifestRules` vocabulary) — a blocks-prop region (e.g. a Section's `body`)\n // receives the paste exactly like the insert dialog and a DnD `into`.\n const manifest = getManifest(node.componentId)\n const childSlot = primaryInsertSlot(manifestRules(manifest))\n if (childSlot) {\n return leashed(\n childSlot.name,\n childSlot,\n { parentId: node.id, slot: childSlot.name },\n `${componentDisplayName(node.componentId, manifest)} · ${childSlot.name}`,\n )\n }\n\n const info = getParentInfo(root, node.id)\n if (info) {\n // A sibling paste is governed by the PARENT's slot leash (the drop rule's\n // before/after case). An unknown parent manifest imposes no allow-list;\n // validation owns that issue.\n const parentSlot = manifestRules(getManifest(info.parent.componentId))?.slots.find((s) => s.name === info.slot)\n return leashed(\n info.slot,\n parentSlot,\n { parentId: info.parent.id, slot: info.slot, index: info.index + 1 },\n `${componentDisplayName(info.parent.componentId, getManifest(info.parent.componentId))} · ${info.slot}`,\n )\n }\n\n // Leaf root with no declared slots (degenerate graphs): append to `children`.\n // No slot rule exists, but the paste still lands at the root — the field leash\n // applies.\n return leashed('children', undefined, { parentId: root.id, slot: 'children' }, 'root')\n}\n"]}
1
+ {"version":3,"file":"paste-target.js","sourceRoot":"","sources":["../../../src/lib/clipboard/paste-target.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAGtE,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAA;AACzG,OAAO,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAA;AAoE3E,MAAM,UAAU,kBAAkB,CAAC,IAalC;IACC,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,GAAG,IAAI,CAAA;IAC1D,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAChF,MAAM,IAAI,GAAG,MAAM,IAAI,IAAI,CAAA;IAE3B,MAAM,OAAO,GAAG,CAAC,IAAY,EAAE,MAAoB,EAAE,gBAAwB,EAAmB,EAAE;QAChG,MAAM,OAAO,GAAG,gBAAgB,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,CAAA;QACnG,IAAI,OAAO,KAAK,aAAa;YAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,WAAW,EAAE,CAAA;QACrF,IAAI,OAAO,KAAK,OAAO;YAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,mBAAmB,EAAE,WAAW,EAAE,CAAA;QACrF,IAAI,OAAO,KAAK,MAAM;YAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,WAAW,EAAE,IAAI,EAAE,gBAAgB,EAAE,CAAA;QACtG,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAA;IAC/C,CAAC,CAAA;IAED,oEAAoE;IACpE,+EAA+E;IAC/E,sEAAsE;IACtE,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;IAC9C,MAAM,SAAS,GAAG,iBAAiB,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,CAAA;IAC5D,IAAI,SAAS,EAAE,CAAC;QACd,OAAO,OAAO,CACZ,SAAS,CAAC,IAAI,EACd,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE,EAC3C,GAAG,oBAAoB,CAAC,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,SAAS,CAAC,IAAI,EAAE,CAC1E,CAAA;IACH,CAAC;IAED,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,CAAA;IACzC,IAAI,IAAI,EAAE,CAAC;QACT,0EAA0E;QAC1E,wEAAwE;QACxE,8BAA8B;QAC9B,OAAO,OAAO,CACZ,IAAI,CAAC,IAAI,EACT,EAAE,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC,EAAE,EACpE,GAAG,oBAAoB,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CACxG,CAAA;IACH,CAAC;IAED,8EAA8E;IAC9E,+EAA+E;IAC/E,WAAW;IACX,OAAO,OAAO,CAAC,UAAU,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,MAAM,CAAC,CAAA;AAC7E,CAAC","sourcesContent":["import { getNode, getParentInfo } from '@elytracms/core/project-graph'\nimport type { ComponentNode, InsertTarget, PlacementScope } from '@elytracms/core/project-graph'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { manifestRules, placementRefusal, primaryInsertSlot } from '@elytracms/studio/lib/dnd/drop-rules'\nimport { componentDisplayName } from '@elytracms/studio/lib/persona/labels'\n\n/**\n * Paste landing-spot resolution (EC-175) — the same placement rule the Insert\n * dialog uses (`insert-context.ts`): a pasted subtree lands *into*\n * the anchor node's child slot when it has one, else *after* the anchor as a\n * sibling, else appended to the page root. Pure and selection-driven so the\n * canvas, the page tree, and keyboard paste all share one rule.\n *\n * EC-545: the resolved slot's `allow` leash is checked HERE, for the pasted\n * subtree's root, through the same `slotAllows` the drop rule reads — so a\n * component drag-and-drop refuses is refused by clipboard too, and a caller\n * cannot reach an `InsertTarget` without passing the check (the rejection is a\n * separate member of the result union, not a flag beside the target). Nested\n * validity of the pasted subtree stays with the publish gate, exactly like a\n * drop.\n *\n * EC-554: the composition FIELD's top-level `allow` is checked here too — a paste\n * that lands as a direct child of the composition root must be in the field's\n * vocabulary. EC-584: both through `placementRefusal`, a reader of the one core\n * placement judge the builder reducer enforces (canvas eligibility included — a\n * server-only root is refused here, COMPOSITION-2); the reducer judges the whole\n * pasted subtree again when it lands.\n */\n\nexport interface PasteTarget {\n ok: true\n target: InsertTarget\n /** Human label of where the paste lands (notices/debugging). */\n destinationLabel: string\n}\n\n/** The resolved slot's own `allow` list excludes the pasted root (EC-545). */\nexport interface PasteSlotRejection {\n ok: false\n code: 'not-allowed'\n /** The pasted root the slot refuses. */\n componentId: string\n /** The resolved slot whose `allow` list excludes it. */\n slot: string\n /** Human label of the refused destination (`Section · body`). */\n destinationLabel: string\n}\n\n/**\n * The composition field's top-level `allow` excludes the pasted root (EC-554).\n * Deliberately carries NO slot/destination label: the refused slot is the\n * invisible synthetic root's (`Stack · children`), and the reason to show is the\n * FIELD — the notice names it from the editor's field scope.\n */\nexport interface PasteFieldRejection {\n ok: false\n code: 'field-not-allowed'\n /** The pasted root the field vocabulary refuses. */\n componentId: string\n}\n\n/** The pasted root is server-only (`clientRenderable: false`) — not canvas-eligible (EC-584). */\nexport interface PasteServerOnlyRejection {\n ok: false\n code: 'server-only'\n componentId: string\n}\n\nexport type PasteRejection = PasteSlotRejection | PasteFieldRejection | PasteServerOnlyRejection\n\nexport type PasteResolution = PasteTarget | PasteRejection\n\nexport function resolvePasteTarget(args: {\n root: ComponentNode\n /** The anchor node (usually the selection); `null` falls back to the root. */\n anchorId: string | null\n /** The pasted subtree's root component — the id the slot leash judges. */\n componentId: string\n getManifest: (componentId: string) => ComponentManifest | undefined\n /**\n * EC-584: what the placement judge reads — the manifest lookup and the composition\n * field's effective top-level vocabulary (the builder store's `placement`).\n * Required so no caller can forget it.\n */\n placement: PlacementScope\n}): PasteResolution {\n const { root, componentId, getManifest, placement } = args\n const anchor = args.anchorId !== null ? getNode(root, args.anchorId) : undefined\n const node = anchor ?? root\n\n const leashed = (slot: string, target: InsertTarget, destinationLabel: string): PasteResolution => {\n const refusal = placementRefusal({ root, parentId: target.parentId, slot, placement, componentId })\n if (refusal === 'server-only') return { ok: false, code: 'server-only', componentId }\n if (refusal === 'field') return { ok: false, code: 'field-not-allowed', componentId }\n if (refusal === 'slot') return { ok: false, code: 'not-allowed', componentId, slot, destinationLabel }\n return { ok: true, target, destinationLabel }\n }\n\n // EC-386: the shared slot rule (`primaryInsertSlot` over the merged\n // `manifestRules` vocabulary) — a blocks-prop region (e.g. a Section's `body`)\n // receives the paste exactly like the insert dialog and a DnD `into`.\n const manifest = getManifest(node.componentId)\n const childSlot = primaryInsertSlot(manifestRules(manifest))\n if (childSlot) {\n return leashed(\n childSlot.name,\n { parentId: node.id, slot: childSlot.name },\n `${componentDisplayName(node.componentId, manifest)} · ${childSlot.name}`,\n )\n }\n\n const info = getParentInfo(root, node.id)\n if (info) {\n // A sibling paste is governed by the PARENT's slot leash (the drop rule's\n // before/after case). An unknown parent manifest imposes no allow-list;\n // validation owns that issue.\n return leashed(\n info.slot,\n { parentId: info.parent.id, slot: info.slot, index: info.index + 1 },\n `${componentDisplayName(info.parent.componentId, getManifest(info.parent.componentId))} · ${info.slot}`,\n )\n }\n\n // Leaf root with no declared slots (degenerate graphs): append to `children`.\n // No slot rule exists, but the paste still lands at the root — the field leash\n // applies.\n return leashed('children', { parentId: root.id, slot: 'children' }, 'root')\n}\n"]}
@@ -0,0 +1,15 @@
1
+ import type { CmsDocument, CollectionDef, DocumentPatch, Locale, ValuePath } from '@elytracms/core/cms-core';
2
+ /**
3
+ * EC-595 A2 — the patch a FIELD write emits at its source.
4
+ *
5
+ * Every writer that writes one field's value through core's `writeField` (the form, a
6
+ * table cell, bulk set-field, an AI field change, a translation, fill-from-base) knows
7
+ * the position it wrote: the field, and — for a localized field — the locale bucket
8
+ * (EC-595 binding point 3: "localized fields patch per bucket"). This reads the value
9
+ * the write left at that position in `written` (the document AFTER `writeField`, so the
10
+ * value is the canonical one, EC-586) and says so as one `set` — or `unset` when the
11
+ * write cleared the key (writing `undefined`, an emptied list).
12
+ */
13
+ export declare function fieldWritePatch(collection: CollectionDef, written: CmsDocument, fieldName: string, locale: Locale | undefined): DocumentPatch;
14
+ /** The position a field write addresses: `[field]`, or `[field, locale]` for a localized field. */
15
+ export declare function fieldPositionPath(collection: CollectionDef, fieldName: string, locale: Locale | undefined): ValuePath;