@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
@@ -0,0 +1,256 @@
1
+ import { applyDocumentPatches, fieldPath, isComponentNodeValue, locateNode, isSequenceValue, nodePath, nodePropPath, semanticValueEqual, } from '@elytracms/core/cms-core';
2
+ function index(content) {
3
+ const byId = new Map();
4
+ const order = [];
5
+ const typography = [];
6
+ let duplicate = false;
7
+ const visit = (list, parent, slot) => {
8
+ let previous;
9
+ list.forEach((entry, at) => {
10
+ if (!isComponentNodeValue(entry)) {
11
+ if (parent === null) {
12
+ previous = { typography: typography.length };
13
+ typography.push(entry);
14
+ }
15
+ return;
16
+ }
17
+ if (byId.has(entry.id))
18
+ duplicate = true;
19
+ const placed = { node: entry, parent, slot, index: at, previous };
20
+ byId.set(entry.id, placed);
21
+ order.push(placed);
22
+ previous = { node: entry.id };
23
+ for (const [name, children] of Object.entries(entry.slots ?? {})) {
24
+ if (Array.isArray(children))
25
+ visit(children, entry.id, name);
26
+ }
27
+ });
28
+ };
29
+ visit(content, null, undefined);
30
+ return { byId, order, typography, duplicate };
31
+ }
32
+ /** Longest common subsequence of two id lists (the siblings that kept their relative order). */
33
+ function lcs(a, b) {
34
+ const rows = a.length + 1;
35
+ const cols = b.length + 1;
36
+ const table = new Array(rows * cols).fill(0);
37
+ for (let i = a.length - 1; i >= 0; i -= 1) {
38
+ for (let j = b.length - 1; j >= 0; j -= 1) {
39
+ table[i * cols + j] =
40
+ a[i] === b[j]
41
+ ? table[(i + 1) * cols + j + 1] + 1
42
+ : Math.max(table[(i + 1) * cols + j], table[i * cols + j + 1]);
43
+ }
44
+ }
45
+ const kept = new Set();
46
+ let i = 0;
47
+ let j = 0;
48
+ while (i < a.length && j < b.length) {
49
+ if (a[i] === b[j]) {
50
+ kept.add(a[i]);
51
+ i += 1;
52
+ j += 1;
53
+ }
54
+ else if (table[(i + 1) * cols + j] >= table[i * cols + j + 1])
55
+ i += 1;
56
+ else
57
+ j += 1;
58
+ }
59
+ return kept;
60
+ }
61
+ /** The keys a node may change without a whole-node rewrite. */
62
+ const STRUCTURAL_KEYS = new Set(['id', 'props', 'slots']);
63
+ /** A static prop (`{ value }`) — the only prop shape a `node.prop` patch carries. */
64
+ function staticValue(prop) {
65
+ if (typeof prop !== 'object' || prop === null || Array.isArray(prop))
66
+ return { ok: false };
67
+ const keys = Object.keys(prop);
68
+ if (keys.length !== 1 || keys[0] !== 'value')
69
+ return { ok: false };
70
+ return { ok: true, value: prop.value };
71
+ }
72
+ /** A new node's subtree without the descendants that already existed (they are moved in). */
73
+ function withoutExisting(node, existed) {
74
+ if (!node.slots)
75
+ return node;
76
+ const slots = {};
77
+ for (const [name, children] of Object.entries(node.slots)) {
78
+ if (!Array.isArray(children))
79
+ continue;
80
+ slots[name] = children
81
+ .filter((child) => !(isComponentNodeValue(child) && existed.has(child.id)))
82
+ .map((child) => (isComponentNodeValue(child) ? withoutExisting(child, existed) : child));
83
+ }
84
+ return { ...node, slots };
85
+ }
86
+ class Coarser extends Error {
87
+ }
88
+ /**
89
+ * The patches that turn the composition field value `before` into `after` (both the
90
+ * stored v2 sequence, `undefined` when absent), addressed at `field` (+ `locale` for a
91
+ * localized field). See the module doc.
92
+ */
93
+ export function compositionPatches(before, after, field, locale) {
94
+ const whole = fieldPath(field, locale);
95
+ if (semanticValueEqual(before, after))
96
+ return [];
97
+ if (after === undefined)
98
+ return [{ op: 'unset', path: whole }];
99
+ const beforeSequence = before === undefined ? { version: 2, content: [] } : before;
100
+ if (!isSequenceValue(beforeSequence) || !isSequenceValue(after))
101
+ return [{ op: 'set', path: whole, value: after }];
102
+ try {
103
+ return nodePatches(beforeSequence.content, after.content, field, locale);
104
+ }
105
+ catch (error) {
106
+ if (!(error instanceof Coarser))
107
+ throw error;
108
+ return [{ op: 'set', path: whole, value: after }];
109
+ }
110
+ }
111
+ function nodePatches(beforeContent, afterContent, field, locale) {
112
+ const was = index(beforeContent);
113
+ const now = index(afterContent);
114
+ if (was.duplicate || now.duplicate)
115
+ throw new Coarser();
116
+ // The canvas never edits typography: a typography change is not a node change.
117
+ if (!semanticValueEqual(was.typography, now.typography))
118
+ throw new Coarser();
119
+ // The structural patches apply in sequence; each position is computed against the
120
+ // value AS IT WILL BE when that patch applies (a simulation with THE merge), so the
121
+ // index is exact for an unchanged store — and anchored AFTER the previous component
122
+ // sibling, which keeps it right when a concurrent edit shifted the sequence.
123
+ let working = { collection: '_', id: '_', values: { [field]: { version: 2, content: [...beforeContent] } } };
124
+ const simulate = (patch) => {
125
+ const result = applyDocumentPatches({ document: working, patches: [patch], since: [] });
126
+ if (result.refused.length > 0)
127
+ throw new Coarser();
128
+ working = result.document;
129
+ };
130
+ const currentArray = (placed) => {
131
+ const value = working.values[field];
132
+ if (!isSequenceValue(value))
133
+ return [];
134
+ if (placed.parent === null)
135
+ return value.content;
136
+ const owner = locateNode(value, placed.parent);
137
+ const slot = owner?.node.slots?.[placed.slot ?? ''];
138
+ return Array.isArray(slot) ? slot : [];
139
+ };
140
+ const position = (placed, moving) => {
141
+ const parent = placed.parent === null ? fieldPath(field, locale) : nodePath(field, locale, placed.parent);
142
+ // A move detaches the node before it is placed: positions are read without it.
143
+ const list = currentArray(placed).filter((entry) => !(isComponentNodeValue(entry) && entry.id === moving));
144
+ let index = 0;
145
+ let anchor;
146
+ const previous = placed.previous;
147
+ if (previous && 'node' in previous) {
148
+ const at = list.findIndex((entry) => isComponentNodeValue(entry) && entry.id === previous.node);
149
+ index = at + 1;
150
+ anchor = previous.node;
151
+ }
152
+ else if (previous && 'typography' in previous) {
153
+ let seen = -1;
154
+ const at = list.findIndex((entry) => !isComponentNodeValue(entry) && ++seen === previous.typography);
155
+ index = at + 1;
156
+ }
157
+ return {
158
+ parent,
159
+ ...(placed.slot !== undefined ? { slot: placed.slot } : {}),
160
+ index: Math.max(0, index),
161
+ ...(anchor !== undefined ? { after: anchor } : {}),
162
+ };
163
+ };
164
+ // Siblings that kept their relative order, per (unchanged) parent array. In the
165
+ // top-level sequence the typography items are fixed separators (no verb moves them),
166
+ // so a component also counts as moved when the number of typography items before it
167
+ // changed — the canvas re-anchors typography by component count on write-back
168
+ // (`mergeBlockRoots`), and the patches must reproduce exactly that value.
169
+ const keptInOrder = new Set();
170
+ const segmentOf = (indexed, placed) => {
171
+ if (placed.parent !== null)
172
+ return 0;
173
+ const list = indexed === was ? beforeContent : afterContent;
174
+ return list.slice(0, placed.index).filter((entry) => !isComponentNodeValue(entry)).length;
175
+ };
176
+ const siblingsOf = (indexed) => {
177
+ const groups = new Map();
178
+ for (const placed of indexed.order) {
179
+ const key = `${placed.parent ?? ''}\u0001${placed.slot ?? ''}`;
180
+ const list = groups.get(key) ?? [];
181
+ list.push({ id: placed.node.id, segment: segmentOf(indexed, placed) });
182
+ groups.set(key, list);
183
+ }
184
+ return groups;
185
+ };
186
+ const wasGroups = siblingsOf(was);
187
+ for (const [key, entries] of siblingsOf(now)) {
188
+ const nowSegment = new Map(entries.map((entry) => [entry.id, entry.segment]));
189
+ const previous = (wasGroups.get(key) ?? [])
190
+ .filter((entry) => nowSegment.get(entry.id) === entry.segment)
191
+ .map((entry) => entry.id);
192
+ const current = entries.map((entry) => entry.id).filter((id) => previous.includes(id));
193
+ for (const id of lcs(previous, current))
194
+ keptInOrder.add(id);
195
+ }
196
+ const structure = [];
197
+ const props = [];
198
+ for (const placed of now.order) {
199
+ const id = placed.node.id;
200
+ const prior = was.byId.get(id);
201
+ if (!prior) {
202
+ const parentIsNew = placed.parent !== null && !was.byId.has(placed.parent);
203
+ if (parentIsNew)
204
+ continue; // carried by its new parent's insert
205
+ const insert = { op: 'insert', at: position(placed), node: withoutExisting(placed.node, was.byId) };
206
+ simulate(insert);
207
+ structure.push(insert);
208
+ continue;
209
+ }
210
+ const parentIsNew = placed.parent !== null && !was.byId.has(placed.parent);
211
+ const moved = parentIsNew || prior.parent !== placed.parent || prior.slot !== placed.slot || !keptInOrder.has(id);
212
+ if (moved) {
213
+ const move = { op: 'move', path: nodePath(field, locale, id), to: position(placed, id) };
214
+ simulate(move);
215
+ structure.push(move);
216
+ }
217
+ // Anything but props / slots changed → no node verb says it.
218
+ for (const key of new Set([...Object.keys(prior.node), ...Object.keys(placed.node)])) {
219
+ if (STRUCTURAL_KEYS.has(key))
220
+ continue;
221
+ const a = prior.node[key];
222
+ const b = placed.node[key];
223
+ if (!semanticValueEqual(a, b)) {
224
+ throw new Coarser();
225
+ }
226
+ }
227
+ const priorProps = prior.node.props ?? {};
228
+ const nextProps = placed.node.props ?? {};
229
+ for (const name of new Set([...Object.keys(priorProps), ...Object.keys(nextProps)])) {
230
+ const a = priorProps[name];
231
+ const b = nextProps[name];
232
+ if (semanticValueEqual(a, b))
233
+ continue;
234
+ const path = nodePropPath(field, locale, id, name);
235
+ if (b === undefined) {
236
+ props.push({ op: 'unset', path });
237
+ continue;
238
+ }
239
+ const value = staticValue(b);
240
+ if (!value.ok)
241
+ throw new Coarser();
242
+ props.push({ op: 'set', path, value: value.value });
243
+ }
244
+ }
245
+ // Removes LAST, top-most only: a child moved out of a removed container has left it.
246
+ const removes = [];
247
+ for (const placed of was.order) {
248
+ if (now.byId.has(placed.node.id))
249
+ continue;
250
+ if (placed.parent !== null && !now.byId.has(placed.parent) && was.byId.has(placed.parent))
251
+ continue;
252
+ removes.push({ op: 'remove', path: nodePath(field, locale, placed.node.id) });
253
+ }
254
+ return [...structure, ...removes, ...props];
255
+ }
256
+ //# sourceMappingURL=composition-patches.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composition-patches.js","sourceRoot":"","sources":["../../../src/components/builder/composition-patches.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n applyDocumentPatches,\n fieldPath,\n isComponentNodeValue,\n locateNode,\n isSequenceValue,\n nodePath,\n nodePropPath,\n semanticValueEqual,\n} from '@elytracms/core/cms-core'\nimport type { CmsDocument, DocumentPatch, SequencePosition, ValuePath } from '@elytracms/core/cms-core'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\n\n/**\n * EC-595 A2 — the CANVAS WRITER's patches: a structural delta of one composition field,\n * keyed by component NODE ID, computed in the canvas write path from the field value\n * before and after one canvas action (insert / remove / duplicate / reorder / move /\n * replace / wrap / setProp / removeProp — and undo / redo, which are just another\n * before → after). It is \"at the source\" (the zone brief): the canvas knows its field\n * and its node ids; nothing diffs a document in the persister.\n *\n * The patches use the value-position grammar's sequence verbs (EC-595 binding point 3):\n *\n * - `insert` — a node new to the field, at its parent sequence (the field's top-level\n * sequence, or a component's named slot) + index, anchored AFTER its previous\n * component sibling; the inserted subtree carries its new descendants and leaves out\n * descendants that already existed (those are `move`d in);\n * - `move` — an existing node whose parent / slot changed, or whose order among its\n * siblings did (the nodes outside the longest common subsequence of the sibling ids);\n * - `remove` — a node gone from the field (only the top-most of a removed subtree);\n * - `set` / `unset` at `node.prop` — a static prop whose value changed.\n *\n * So two sessions inserting into one page both land (two `insert`s into one parent\n * never conflict); a prop edit on node X and a move of node Y in another session both\n * apply; only a change to the SAME node position conflicts (the merge's prefix rule).\n *\n * When a change cannot be said with node verbs — the typography items (which the canvas\n * never edits) differ, a node changes something other than its props / slots, a prop is\n * not a static value, duplicate ids — the delta is the coarser legal path: the whole\n * field (`set` / `unset`), and conflict detection is correspondingly coarser, never\n * wrong (value-path.ts: \"writers may always emit a coarser path\").\n */\n\ninterface Placed {\n readonly node: ComponentNode\n /** `null` = the field's top-level sequence. */\n readonly parent: string | null\n readonly slot: string | undefined\n /** The index in the parent array (the full top-level `content`, or the slot list). */\n readonly index: number\n /** The entry immediately before it in the parent array: a component id, or the ordinal of a typography item. */\n readonly previous: { readonly node: string } | { readonly typography: number } | undefined\n}\n\ninterface Indexed {\n readonly byId: Map<string, Placed>\n /** Pre-order. */\n readonly order: readonly Placed[]\n readonly typography: readonly unknown[]\n readonly duplicate: boolean\n}\n\nfunction index(content: readonly unknown[]): Indexed {\n const byId = new Map<string, Placed>()\n const order: Placed[] = []\n const typography: unknown[] = []\n let duplicate = false\n const visit = (list: readonly unknown[], parent: string | null, slot: string | undefined) => {\n let previous: Placed['previous']\n list.forEach((entry, at) => {\n if (!isComponentNodeValue(entry)) {\n if (parent === null) {\n previous = { typography: typography.length }\n typography.push(entry)\n }\n return\n }\n if (byId.has(entry.id)) duplicate = true\n const placed: Placed = { node: entry, parent, slot, index: at, previous }\n byId.set(entry.id, placed)\n order.push(placed)\n previous = { node: entry.id }\n for (const [name, children] of Object.entries(entry.slots ?? {})) {\n if (Array.isArray(children)) visit(children, entry.id, name)\n }\n })\n }\n visit(content, null, undefined)\n return { byId, order, typography, duplicate }\n}\n\n/** Longest common subsequence of two id lists (the siblings that kept their relative order). */\nfunction lcs(a: readonly string[], b: readonly string[]): Set<string> {\n const rows = a.length + 1\n const cols = b.length + 1\n const table: number[] = new Array(rows * cols).fill(0)\n for (let i = a.length - 1; i >= 0; i -= 1) {\n for (let j = b.length - 1; j >= 0; j -= 1) {\n table[i * cols + j] =\n a[i] === b[j]\n ? table[(i + 1) * cols + j + 1]! + 1\n : Math.max(table[(i + 1) * cols + j]!, table[i * cols + j + 1]!)\n }\n }\n const kept = new Set<string>()\n let i = 0\n let j = 0\n while (i < a.length && j < b.length) {\n if (a[i] === b[j]) {\n kept.add(a[i]!)\n i += 1\n j += 1\n } else if (table[(i + 1) * cols + j]! >= table[i * cols + j + 1]!) i += 1\n else j += 1\n }\n return kept\n}\n\n/** The keys a node may change without a whole-node rewrite. */\nconst STRUCTURAL_KEYS = new Set(['id', 'props', 'slots'])\n\n/** A static prop (`{ value }`) — the only prop shape a `node.prop` patch carries. */\nfunction staticValue(prop: unknown): { ok: true; value: unknown } | { ok: false } {\n if (typeof prop !== 'object' || prop === null || Array.isArray(prop)) return { ok: false }\n const keys = Object.keys(prop)\n if (keys.length !== 1 || keys[0] !== 'value') return { ok: false }\n return { ok: true, value: (prop as { value: unknown }).value }\n}\n\n/** A new node's subtree without the descendants that already existed (they are moved in). */\nfunction withoutExisting(node: ComponentNode, existed: ReadonlyMap<string, Placed>): ComponentNode {\n if (!node.slots) return node\n const slots: Record<string, ComponentNode[]> = {}\n for (const [name, children] of Object.entries(node.slots)) {\n if (!Array.isArray(children)) continue\n slots[name] = children\n .filter((child) => !(isComponentNodeValue(child) && existed.has(child.id)))\n .map((child) => (isComponentNodeValue(child) ? withoutExisting(child, existed) : child))\n }\n return { ...node, slots }\n}\n\nclass Coarser extends Error {}\n\n/**\n * The patches that turn the composition field value `before` into `after` (both the\n * stored v2 sequence, `undefined` when absent), addressed at `field` (+ `locale` for a\n * localized field). See the module doc.\n */\nexport function compositionPatches(\n before: unknown,\n after: unknown,\n field: string,\n locale: string | undefined,\n): DocumentPatch[] {\n const whole = fieldPath(field, locale)\n if (semanticValueEqual(before, after)) return []\n if (after === undefined) return [{ op: 'unset', path: whole }]\n const beforeSequence = before === undefined ? { version: 2 as const, content: [] } : before\n if (!isSequenceValue(beforeSequence) || !isSequenceValue(after)) return [{ op: 'set', path: whole, value: after }]\n try {\n return nodePatches(beforeSequence.content, after.content, field, locale)\n } catch (error) {\n if (!(error instanceof Coarser)) throw error\n return [{ op: 'set', path: whole, value: after }]\n }\n}\n\nfunction nodePatches(\n beforeContent: readonly unknown[],\n afterContent: readonly unknown[],\n field: string,\n locale: string | undefined,\n): DocumentPatch[] {\n const was = index(beforeContent)\n const now = index(afterContent)\n if (was.duplicate || now.duplicate) throw new Coarser()\n // The canvas never edits typography: a typography change is not a node change.\n if (!semanticValueEqual(was.typography, now.typography)) throw new Coarser()\n\n // The structural patches apply in sequence; each position is computed against the\n // value AS IT WILL BE when that patch applies (a simulation with THE merge), so the\n // index is exact for an unchanged store — and anchored AFTER the previous component\n // sibling, which keeps it right when a concurrent edit shifted the sequence.\n let working: CmsDocument = { collection: '_', id: '_', values: { [field]: { version: 2, content: [...beforeContent] } } }\n const simulate = (patch: DocumentPatch) => {\n const result = applyDocumentPatches({ document: working, patches: [patch], since: [] })\n if (result.refused.length > 0) throw new Coarser()\n working = result.document\n }\n const currentArray = (placed: Placed): readonly unknown[] => {\n const value = working.values[field]\n if (!isSequenceValue(value)) return []\n if (placed.parent === null) return value.content\n const owner = locateNode(value, placed.parent)\n const slot = (owner?.node.slots as Record<string, unknown> | undefined)?.[placed.slot ?? '']\n return Array.isArray(slot) ? slot : []\n }\n const position = (placed: Placed, moving?: string): SequencePosition => {\n const parent: ValuePath = placed.parent === null ? fieldPath(field, locale) : nodePath(field, locale, placed.parent)\n // A move detaches the node before it is placed: positions are read without it.\n const list = currentArray(placed).filter((entry) => !(isComponentNodeValue(entry) && entry.id === moving))\n let index = 0\n let anchor: string | undefined\n const previous = placed.previous\n if (previous && 'node' in previous) {\n const at = list.findIndex((entry) => isComponentNodeValue(entry) && entry.id === previous.node)\n index = at + 1\n anchor = previous.node\n } else if (previous && 'typography' in previous) {\n let seen = -1\n const at = list.findIndex((entry) => !isComponentNodeValue(entry) && ++seen === previous.typography)\n index = at + 1\n }\n return {\n parent,\n ...(placed.slot !== undefined ? { slot: placed.slot } : {}),\n index: Math.max(0, index),\n ...(anchor !== undefined ? { after: anchor } : {}),\n }\n }\n\n // Siblings that kept their relative order, per (unchanged) parent array. In the\n // top-level sequence the typography items are fixed separators (no verb moves them),\n // so a component also counts as moved when the number of typography items before it\n // changed — the canvas re-anchors typography by component count on write-back\n // (`mergeBlockRoots`), and the patches must reproduce exactly that value.\n const keptInOrder = new Set<string>()\n const segmentOf = (indexed: Indexed, placed: Placed) => {\n if (placed.parent !== null) return 0\n const list = indexed === was ? beforeContent : afterContent\n return list.slice(0, placed.index).filter((entry) => !isComponentNodeValue(entry)).length\n }\n const siblingsOf = (indexed: Indexed) => {\n const groups = new Map<string, { id: string; segment: number }[]>()\n for (const placed of indexed.order) {\n const key = `${placed.parent ?? ''}\\u0001${placed.slot ?? ''}`\n const list = groups.get(key) ?? []\n list.push({ id: placed.node.id, segment: segmentOf(indexed, placed) })\n groups.set(key, list)\n }\n return groups\n }\n const wasGroups = siblingsOf(was)\n for (const [key, entries] of siblingsOf(now)) {\n const nowSegment = new Map(entries.map((entry) => [entry.id, entry.segment]))\n const previous = (wasGroups.get(key) ?? [])\n .filter((entry) => nowSegment.get(entry.id) === entry.segment)\n .map((entry) => entry.id)\n const current = entries.map((entry) => entry.id).filter((id) => previous.includes(id))\n for (const id of lcs(previous, current)) keptInOrder.add(id)\n }\n\n const structure: DocumentPatch[] = []\n const props: DocumentPatch[] = []\n for (const placed of now.order) {\n const id = placed.node.id\n const prior = was.byId.get(id)\n if (!prior) {\n const parentIsNew = placed.parent !== null && !was.byId.has(placed.parent)\n if (parentIsNew) continue // carried by its new parent's insert\n const insert: DocumentPatch = { op: 'insert', at: position(placed), node: withoutExisting(placed.node, was.byId) }\n simulate(insert)\n structure.push(insert)\n continue\n }\n const parentIsNew = placed.parent !== null && !was.byId.has(placed.parent)\n const moved =\n parentIsNew || prior.parent !== placed.parent || prior.slot !== placed.slot || !keptInOrder.has(id)\n if (moved) {\n const move: DocumentPatch = { op: 'move', path: nodePath(field, locale, id), to: position(placed, id) }\n simulate(move)\n structure.push(move)\n }\n // Anything but props / slots changed → no node verb says it.\n for (const key of new Set([...Object.keys(prior.node), ...Object.keys(placed.node)])) {\n if (STRUCTURAL_KEYS.has(key)) continue\n const a = (prior.node as unknown as Record<string, unknown>)[key]\n const b = (placed.node as unknown as Record<string, unknown>)[key]\n if (!semanticValueEqual(a, b)) {\n throw new Coarser()\n }\n }\n const priorProps = prior.node.props ?? {}\n const nextProps = placed.node.props ?? {}\n for (const name of new Set([...Object.keys(priorProps), ...Object.keys(nextProps)])) {\n const a = (priorProps as Record<string, unknown>)[name]\n const b = (nextProps as Record<string, unknown>)[name]\n if (semanticValueEqual(a, b)) continue\n const path = nodePropPath(field, locale, id, name)\n if (b === undefined) {\n props.push({ op: 'unset', path })\n continue\n }\n const value = staticValue(b)\n if (!value.ok) throw new Coarser()\n props.push({ op: 'set', path, value: value.value })\n }\n }\n\n // Removes LAST, top-most only: a child moved out of a removed container has left it.\n const removes: DocumentPatch[] = []\n for (const placed of was.order) {\n if (now.byId.has(placed.node.id)) continue\n if (placed.parent !== null && !now.byId.has(placed.parent) && was.byId.has(placed.parent)) continue\n removes.push({ op: 'remove', path: nodePath(field, locale, placed.node.id) })\n }\n return [...structure, ...removes, ...props]\n}\n"]}
@@ -23,6 +23,11 @@ export interface DeleteKeyListenerOptions {
23
23
  getRootId: () => string | null;
24
24
  /** Extra suppression (e.g. a read-only published preview). */
25
25
  isDisabled?: () => boolean;
26
+ /**
27
+ * EC-549: remove the selected typography RUN, when one is selected (it takes
28
+ * precedence — a run and a node are never both selected); returns whether it did.
29
+ */
30
+ removeSelectedRun?: () => boolean;
26
31
  }
27
32
  interface ListenerTarget {
28
33
  addEventListener(type: string, listener: (e: Event) => void): void;
@@ -34,6 +34,10 @@ export function attachDeleteKeyListener(target, opts) {
34
34
  return;
35
35
  if (isEditingContext(event.target))
36
36
  return;
37
+ if (opts.removeSelectedRun?.()) {
38
+ event.preventDefault();
39
+ return;
40
+ }
37
41
  const selectedId = opts.getSelectedId();
38
42
  if (!selectedId || selectedId === opts.getRootId())
39
43
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"delete-key.js","sourceRoot":"","sources":["../../../src/components/builder/delete-key.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAE3D;;;;;;;;GAQG;AAEH;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAA0B;IACzD,OAAO,qBAAqB,CAAC,MAAM,CAAC,CAAA;AACtC,CAAC;AAkBD;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CACrC,MAAsB,EACtB,IAA8B;IAE9B,MAAM,SAAS,GAAG,CAAC,CAAQ,EAAQ,EAAE;QACnC,MAAM,KAAK,GAAG,CAAkB,CAAA;QAChC,IAAI,KAAK,CAAC,GAAG,KAAK,QAAQ,IAAI,KAAK,CAAC,GAAG,KAAK,WAAW;YAAE,OAAM;QAC/D,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,MAAM;YAAE,OAAM;QAC1D,IAAI,IAAI,CAAC,UAAU,EAAE,EAAE,IAAI,KAAK;YAAE,OAAM;QACxC,IAAI,gBAAgB,CAAC,KAAK,CAAC,MAAM,CAAC;YAAE,OAAM;QAC1C,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa,EAAE,CAAA;QACvC,IAAI,CAAC,UAAU,IAAI,UAAU,KAAK,IAAI,CAAC,SAAS,EAAE;YAAE,OAAM;QAC1D,KAAK,CAAC,cAAc,EAAE,CAAA;QACtB,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;IACzB,CAAC,CAAA;IACD,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;IAC7C,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;AAC/D,CAAC","sourcesContent":["import { clipboardEventBlocked } from './clipboard-actions'\n\n/**\n * Delete/Backspace removes the selected node (EC-404, scope 3) — the gap the\n * findings batch surfaced: the studio had no keyboard remove at all except cut\n * (⌘X). This sits beside the clipboard listeners (`attachClipboardListeners`),\n * wired on the same two surfaces (the studio window for the page tree, the\n * iframe document for the canvas), and reuses the SAME editing-context guard so\n * pressing Backspace while typing in an input or an inline text/rich-text editor\n * edits the text — never deletes the block.\n */\n\n/**\n * True when a key event's target owns its own text-editing behavior — a form\n * field or any contenteditable (which includes the EC-158/EC-159 inline\n * editors). The single guard for the Delete/Backspace remove, kept identical to\n * the clipboard shortcuts' scope so the two can never disagree.\n */\nexport function isEditingContext(target: EventTarget | null): boolean {\n return clipboardEventBlocked(target)\n}\n\nexport interface DeleteKeyListenerOptions {\n /** The current selection — the remove subject. */\n getSelectedId: () => string | null\n /** Dispatch the remove (the existing `remove` tree action; one undo restores). */\n remove: (id: string) => void\n /** The id that must never be removed (the composition root). */\n getRootId: () => string | null\n /** Extra suppression (e.g. a read-only published preview). */\n isDisabled?: () => boolean\n}\n\ninterface ListenerTarget {\n addEventListener(type: string, listener: (e: Event) => void): void\n removeEventListener(type: string, listener: (e: Event) => void): void\n}\n\n/**\n * Wire Delete/Backspace → remove the selected node. Inert when the event\n * targets an editing context, when nothing (or the root) is selected, or when\n * disabled. Modifier combinations are ignored so it never shadows a browser\n * shortcut (⌘Delete etc.).\n */\nexport function attachDeleteKeyListener(\n target: ListenerTarget,\n opts: DeleteKeyListenerOptions,\n): () => void {\n const onKeyDown = (e: Event): void => {\n const event = e as KeyboardEvent\n if (event.key !== 'Delete' && event.key !== 'Backspace') return\n if (event.metaKey || event.ctrlKey || event.altKey) return\n if (opts.isDisabled?.() ?? false) return\n if (isEditingContext(event.target)) return\n const selectedId = opts.getSelectedId()\n if (!selectedId || selectedId === opts.getRootId()) return\n event.preventDefault()\n opts.remove(selectedId)\n }\n target.addEventListener('keydown', onKeyDown)\n return () => target.removeEventListener('keydown', onKeyDown)\n}\n"]}
1
+ {"version":3,"file":"delete-key.js","sourceRoot":"","sources":["../../../src/components/builder/delete-key.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAE3D;;;;;;;;GAQG;AAEH;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAA0B;IACzD,OAAO,qBAAqB,CAAC,MAAM,CAAC,CAAA;AACtC,CAAC;AAuBD;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CACrC,MAAsB,EACtB,IAA8B;IAE9B,MAAM,SAAS,GAAG,CAAC,CAAQ,EAAQ,EAAE;QACnC,MAAM,KAAK,GAAG,CAAkB,CAAA;QAChC,IAAI,KAAK,CAAC,GAAG,KAAK,QAAQ,IAAI,KAAK,CAAC,GAAG,KAAK,WAAW;YAAE,OAAM;QAC/D,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,MAAM;YAAE,OAAM;QAC1D,IAAI,IAAI,CAAC,UAAU,EAAE,EAAE,IAAI,KAAK;YAAE,OAAM;QACxC,IAAI,gBAAgB,CAAC,KAAK,CAAC,MAAM,CAAC;YAAE,OAAM;QAC1C,IAAI,IAAI,CAAC,iBAAiB,EAAE,EAAE,EAAE,CAAC;YAC/B,KAAK,CAAC,cAAc,EAAE,CAAA;YACtB,OAAM;QACR,CAAC;QACD,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa,EAAE,CAAA;QACvC,IAAI,CAAC,UAAU,IAAI,UAAU,KAAK,IAAI,CAAC,SAAS,EAAE;YAAE,OAAM;QAC1D,KAAK,CAAC,cAAc,EAAE,CAAA;QACtB,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;IACzB,CAAC,CAAA;IACD,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;IAC7C,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;AAC/D,CAAC","sourcesContent":["import { clipboardEventBlocked } from './clipboard-actions'\n\n/**\n * Delete/Backspace removes the selected node (EC-404, scope 3) — the gap the\n * findings batch surfaced: the studio had no keyboard remove at all except cut\n * (⌘X). This sits beside the clipboard listeners (`attachClipboardListeners`),\n * wired on the same two surfaces (the studio window for the page tree, the\n * iframe document for the canvas), and reuses the SAME editing-context guard so\n * pressing Backspace while typing in an input or an inline text/rich-text editor\n * edits the text — never deletes the block.\n */\n\n/**\n * True when a key event's target owns its own text-editing behavior — a form\n * field or any contenteditable (which includes the EC-158/EC-159 inline\n * editors). The single guard for the Delete/Backspace remove, kept identical to\n * the clipboard shortcuts' scope so the two can never disagree.\n */\nexport function isEditingContext(target: EventTarget | null): boolean {\n return clipboardEventBlocked(target)\n}\n\nexport interface DeleteKeyListenerOptions {\n /** The current selection — the remove subject. */\n getSelectedId: () => string | null\n /** Dispatch the remove (the existing `remove` tree action; one undo restores). */\n remove: (id: string) => void\n /** The id that must never be removed (the composition root). */\n getRootId: () => string | null\n /** Extra suppression (e.g. a read-only published preview). */\n isDisabled?: () => boolean\n /**\n * EC-549: remove the selected typography RUN, when one is selected (it takes\n * precedence — a run and a node are never both selected); returns whether it did.\n */\n removeSelectedRun?: () => boolean\n}\n\ninterface ListenerTarget {\n addEventListener(type: string, listener: (e: Event) => void): void\n removeEventListener(type: string, listener: (e: Event) => void): void\n}\n\n/**\n * Wire Delete/Backspace → remove the selected node. Inert when the event\n * targets an editing context, when nothing (or the root) is selected, or when\n * disabled. Modifier combinations are ignored so it never shadows a browser\n * shortcut (⌘Delete etc.).\n */\nexport function attachDeleteKeyListener(\n target: ListenerTarget,\n opts: DeleteKeyListenerOptions,\n): () => void {\n const onKeyDown = (e: Event): void => {\n const event = e as KeyboardEvent\n if (event.key !== 'Delete' && event.key !== 'Backspace') return\n if (event.metaKey || event.ctrlKey || event.altKey) return\n if (opts.isDisabled?.() ?? false) return\n if (isEditingContext(event.target)) return\n if (opts.removeSelectedRun?.()) {\n event.preventDefault()\n return\n }\n const selectedId = opts.getSelectedId()\n if (!selectedId || selectedId === opts.getRootId()) return\n event.preventDefault()\n opts.remove(selectedId)\n }\n target.addEventListener('keydown', onKeyDown)\n return () => target.removeEventListener('keydown', onKeyDown)\n}\n"]}
@@ -1,4 +1,4 @@
1
- import type { ComponentNode } from '@elytracms/core/project-graph';
1
+ import type { ComponentNode, PlacementScope } from '@elytracms/core/project-graph';
2
2
  import type { StudioRegistry } from '@elytracms/studio/lib/registry';
3
3
  import type { InsertContext } from './insert-menu.js';
4
4
  /**
@@ -9,22 +9,18 @@ import type { InsertContext } from './insert-menu.js';
9
9
  * Canvas editor (`composition-field-editor.tsx`, EC-188/AD-12) can own it without
10
10
  * importing the dead shell. No UI here — pure graph/registry resolution.
11
11
  */
12
+ /** What the resolution reads from the builder store: the tree and its placement scope (EC-584). */
13
+ export interface InsertScope {
14
+ present: ComponentNode;
15
+ placement: PlacementScope;
16
+ }
12
17
  /**
13
- * Resolve where an insert lands: into the node's child slot, else as a sibling.
14
- *
15
- * `fieldAllow` (EC-188) is the composition field's TOP-LEVEL vocabulary
16
- * (`field.allow`). It constrains ONLY inserts that land as a direct child of the
17
- * active page root — the top level of a composition field value — where it is
18
- * intersected into `slotAllow` so forbidden components are never even offered,
19
- * mirroring the top-level leash in `validateCompositionValue`. Deeper inserts keep
20
- * following each component's own slot `allow`. Callers that omit `fieldAllow` get
21
- * an unconstrained top level. EC-554: which placements the field governs is read
22
- * through `fieldAllowAt` — the SAME reader drag-and-drop and paste judge with, so
23
- * the offer and the placement can never disagree.
18
+ * Resolve where an insert lands: into the node's child slot, else as a sibling. The
19
+ * offered vocabulary is the landing position's (see {@link contextAt}) — the SAME
20
+ * reader the reducer, drag-and-drop and paste judge with, so the offer and the
21
+ * placement can never disagree.
24
22
  */
25
- export declare function resolveInsertContext(builder: {
26
- present: ComponentNode;
27
- }, nodeId: string, fieldAllow?: readonly string[], studio?: StudioRegistry): InsertContext | null;
23
+ export declare function resolveInsertContext(builder: InsertScope, nodeId: string, studio?: StudioRegistry): InsertContext | null;
28
24
  /**
29
25
  * An in-canvas insertion position relative to a reference node (EC-405) — the
30
26
  * DnD position vocabulary (`lib/dnd/drop-rules`), narrowed to the three the
@@ -42,17 +38,15 @@ export type InsertPosition = 'before' | 'after' | 'into';
42
38
  * {@link primaryInsertSlot} (the same slot a drop `into` picks; `null` when the
43
39
  * node accepts no children), `before`/`after` use the reference's parent slot at
44
40
  * `index`/`index + 1` — so this surface and drag-insert can never disagree about
45
- * where a position lands or what it allows. `fieldAllow` (EC-188) still constrains
46
- * ONLY a direct child of the composition root, mirroring `resolveInsertContext`.
41
+ * where a position lands or what it allows (EC-584: the vocabulary is `allowAt`'s,
42
+ * exactly as in `resolveInsertContext`).
47
43
  */
48
- export declare function resolveInsertContextAt(builder: {
49
- present: ComponentNode;
50
- }, nodeId: string, position: InsertPosition, fieldAllow?: readonly string[], studio?: StudioRegistry): InsertContext | null;
44
+ export declare function resolveInsertContextAt(builder: InsertScope, nodeId: string, position: InsertPosition, studio?: StudioRegistry): InsertContext | null;
51
45
  /**
52
46
  * Which affordance positions are actually insertable at `nodeId` (EC-405 scope 3):
53
47
  * a position is offered only when it structurally exists AND at least one registry
54
- * component may land there (the SAME slot-rule filter the tree/DnD and the blocks
55
- * panel apply — {@link hasInsertableComponent} over the resolved `slotAllow`), so a
48
+ * component may land there (the SAME judge the tree/DnD, the blocks panel and the
49
+ * reducer apply — {@link hasInsertableComponent} over the resolved `allow`), so a
56
50
  * blocked spot shows no dead button.
57
51
  *
58
52
  * An EMPTY, insertable container surfaces ONLY `into` — the meaningful action there
@@ -62,6 +56,4 @@ export declare function resolveInsertContextAt(builder: {
62
56
  * child already carries its own edge affordances, so a filled container needs no
63
57
  * center "+").
64
58
  */
65
- export declare function insertablePositions(builder: {
66
- present: ComponentNode;
67
- }, nodeId: string, fieldAllow?: readonly string[], studio?: StudioRegistry): InsertPosition[];
59
+ export declare function insertablePositions(builder: InsertScope, nodeId: string, studio?: StudioRegistry): InsertPosition[];
@@ -1,30 +1,29 @@
1
- import { getNode, getParentInfo } from '@elytracms/core/project-graph';
1
+ import { allowAt, getNode, getParentInfo } from '@elytracms/core/project-graph';
2
2
  import { getStudioRegistry } from '@elytracms/studio/lib/registry';
3
3
  import { componentDisplayName } from '@elytracms/studio/lib/persona/labels';
4
- import { fieldAllowAt, manifestRules, primaryInsertSlot } from '@elytracms/studio/lib/dnd/drop-rules';
5
- import { hasInsertableComponent, intersectAllow } from './blocks-model.js';
4
+ import { manifestRules, primaryInsertSlot } from '@elytracms/studio/lib/dnd/drop-rules';
5
+ import { hasInsertableComponent } from './blocks-model.js';
6
6
  /**
7
- * Where an insert lands: the canvas insert-context resolution, shared by every
8
- * surface that mounts the canvas engine.
9
- *
10
- * Extracted from the retired standalone `BuilderShell` (EC-210) so the in-CMS
11
- * Canvas editor (`composition-field-editor.tsx`, EC-188/AD-12) can own it without
12
- * importing the dead shell. No UI here — pure graph/registry resolution.
7
+ * The offer at a target: its EFFECTIVE vocabulary, read through the core judge's own
8
+ * reader (`allowAt` — the composition field's at the composition root, keyed on node
9
+ * identity, mirroring the top-level leash in `validateCompositionValue`; each slot's
10
+ * own `allow` below), so the offer is exactly what the reducer admits (EC-584).
11
+ * EC-544: a closed set that admits nothing is the "No allowed components match" state.
13
12
  */
13
+ function contextAt(builder, target, destinationLabel) {
14
+ return {
15
+ target,
16
+ allow: allowAt(builder.present, target.parentId, target.slot, builder.placement),
17
+ destinationLabel,
18
+ };
19
+ }
14
20
  /**
15
- * Resolve where an insert lands: into the node's child slot, else as a sibling.
16
- *
17
- * `fieldAllow` (EC-188) is the composition field's TOP-LEVEL vocabulary
18
- * (`field.allow`). It constrains ONLY inserts that land as a direct child of the
19
- * active page root — the top level of a composition field value — where it is
20
- * intersected into `slotAllow` so forbidden components are never even offered,
21
- * mirroring the top-level leash in `validateCompositionValue`. Deeper inserts keep
22
- * following each component's own slot `allow`. Callers that omit `fieldAllow` get
23
- * an unconstrained top level. EC-554: which placements the field governs is read
24
- * through `fieldAllowAt` — the SAME reader drag-and-drop and paste judge with, so
25
- * the offer and the placement can never disagree.
21
+ * Resolve where an insert lands: into the node's child slot, else as a sibling. The
22
+ * offered vocabulary is the landing position's (see {@link contextAt}) — the SAME
23
+ * reader the reducer, drag-and-drop and paste judge with, so the offer and the
24
+ * placement can never disagree.
26
25
  */
27
- export function resolveInsertContext(builder, nodeId, fieldAllow, studio = getStudioRegistry()) {
26
+ export function resolveInsertContext(builder, nodeId, studio = getStudioRegistry()) {
28
27
  const root = builder.present;
29
28
  const node = getNode(root, nodeId);
30
29
  if (!node)
@@ -35,47 +34,24 @@ export function resolveInsertContext(builder, nodeId, fieldAllow, studio = getSt
35
34
  // instead of silently degrading to a sibling insert.
36
35
  const manifest = studio.registry.getManifest(node.componentId);
37
36
  const childSlot = primaryInsertSlot(manifestRules(manifest));
38
- const ctx = childSlot
39
- ? {
40
- target: { parentId: nodeId, slot: childSlot.name },
41
- slotAllow: childSlot.allow,
42
- destinationLabel: `${componentDisplayName(node.componentId, manifest)} · ${childSlot.name}`,
43
- }
44
- : resolveSiblingContext(studio, root, nodeId);
45
- // The field vocabulary governs only the top level (a direct child of the page
46
- // root). EC-544: an `allow` list is a closed set — absent = any, a list (even
47
- // an empty one) = exactly those ids — the same reading the validator applies,
48
- // so an empty intersection here is the "No allowed components match" state.
49
- return withFieldLeash(root, ctx, fieldAllow);
50
- }
51
- /**
52
- * Layer the composition field's top-level vocabulary over the resolved slot
53
- * `allow` (EC-188) — for a direct child of the root only, as decided by
54
- * {@link fieldAllowAt} (EC-554: the one reader every placement path shares).
55
- */
56
- function withFieldLeash(root, ctx, fieldAllow) {
57
- const leash = fieldAllowAt(root, ctx.target.parentId, fieldAllow);
58
- return leash ? { ...ctx, slotAllow: intersectAllow(ctx.slotAllow, leash) } : ctx;
37
+ if (childSlot) {
38
+ return contextAt(builder, { parentId: nodeId, slot: childSlot.name }, `${componentDisplayName(node.componentId, manifest)} · ${childSlot.name}`);
39
+ }
40
+ return resolveSiblingContext(studio, builder, nodeId);
59
41
  }
60
42
  /** A destination's component label — its manifest title (EC-591: never a stripped id prefix). */
61
43
  function parentLabel(studio, componentId) {
62
44
  return componentDisplayName(componentId, studio.registry.getManifest(componentId));
63
45
  }
64
46
  /** Sibling-or-root insert context for a node with no insertable child slot. */
65
- function resolveSiblingContext(studio, root, nodeId) {
66
- const info = getParentInfo(root, nodeId);
47
+ function resolveSiblingContext(studio, builder, nodeId) {
48
+ const info = getParentInfo(builder.present, nodeId);
67
49
  if (info) {
68
- // EC-386: the parent slot may be a blocks-prop region — look it up on the
69
- // merged vocabulary so its `allow` leash still filters sibling inserts.
70
- const pManifest = manifestRules(studio.registry.getManifest(info.parent.componentId));
71
- const allow = pManifest?.slots.find((s) => s.name === info.slot)?.allow;
72
- return {
73
- target: { parentId: info.parent.id, slot: info.slot, index: info.index + 1 },
74
- slotAllow: allow,
75
- destinationLabel: `${parentLabel(studio, info.parent.componentId)} · ${info.slot}`,
76
- };
50
+ // EC-386: the parent slot may be a blocks-prop region — its `allow` leash still
51
+ // filters sibling inserts (the core lookup merges blocks props into the slots).
52
+ return contextAt(builder, { parentId: info.parent.id, slot: info.slot, index: info.index + 1 }, `${parentLabel(studio, info.parent.componentId)} · ${info.slot}`);
77
53
  }
78
- return { target: { parentId: nodeId, slot: 'children' }, destinationLabel: 'root' };
54
+ return contextAt(builder, { parentId: nodeId, slot: 'children' }, 'root');
79
55
  }
80
56
  /**
81
57
  * Resolve an `InsertContext` for an EXPLICIT position relative to `nodeId`
@@ -87,15 +63,14 @@ function resolveSiblingContext(studio, root, nodeId) {
87
63
  * {@link primaryInsertSlot} (the same slot a drop `into` picks; `null` when the
88
64
  * node accepts no children), `before`/`after` use the reference's parent slot at
89
65
  * `index`/`index + 1` — so this surface and drag-insert can never disagree about
90
- * where a position lands or what it allows. `fieldAllow` (EC-188) still constrains
91
- * ONLY a direct child of the composition root, mirroring `resolveInsertContext`.
66
+ * where a position lands or what it allows (EC-584: the vocabulary is `allowAt`'s,
67
+ * exactly as in `resolveInsertContext`).
92
68
  */
93
- export function resolveInsertContextAt(builder, nodeId, position, fieldAllow, studio = getStudioRegistry()) {
69
+ export function resolveInsertContextAt(builder, nodeId, position, studio = getStudioRegistry()) {
94
70
  const root = builder.present;
95
71
  const node = getNode(root, nodeId);
96
72
  if (!node)
97
73
  return null;
98
- let ctx;
99
74
  if (position === 'into') {
100
75
  // EC-386: `manifestRules` folds blocks-prop regions into the slot set, so
101
76
  // an affordance/drop `into` a Section-style body lands in that region.
@@ -103,40 +78,18 @@ export function resolveInsertContextAt(builder, nodeId, position, fieldAllow, st
103
78
  const slot = primaryInsertSlot(manifestRules(manifest));
104
79
  if (!slot)
105
80
  return null; // the node accepts no children — no `into` position
106
- ctx = {
107
- target: {
108
- parentId: nodeId,
109
- slot: slot.name,
110
- index: (node.slots?.[slot.name] ?? []).length,
111
- },
112
- slotAllow: slot.allow,
113
- destinationLabel: `${componentDisplayName(node.componentId, manifest)} · ${slot.name}`,
114
- };
81
+ return contextAt(builder, { parentId: nodeId, slot: slot.name, index: (node.slots?.[slot.name] ?? []).length }, `${componentDisplayName(node.componentId, manifest)} · ${slot.name}`);
115
82
  }
116
- else {
117
- const info = getParentInfo(root, nodeId);
118
- if (!info)
119
- return null; // the root has no siblings — no before/after position
120
- // EC-386: merged vocabulary — a blocks-prop parent region keeps its leash.
121
- const pManifest = manifestRules(studio.registry.getManifest(info.parent.componentId));
122
- ctx = {
123
- target: {
124
- parentId: info.parent.id,
125
- slot: info.slot,
126
- index: info.index + (position === 'after' ? 1 : 0),
127
- },
128
- slotAllow: pManifest?.slots.find((s) => s.name === info.slot)?.allow,
129
- destinationLabel: `${parentLabel(studio, info.parent.componentId)} · ${info.slot}`,
130
- };
131
- }
132
- // EC-544: closed-set reading, same as `resolveInsertContext` above.
133
- return withFieldLeash(root, ctx, fieldAllow);
83
+ const info = getParentInfo(root, nodeId);
84
+ if (!info)
85
+ return null; // the root has no siblings — no before/after position
86
+ return contextAt(builder, { parentId: info.parent.id, slot: info.slot, index: info.index + (position === 'after' ? 1 : 0) }, `${parentLabel(studio, info.parent.componentId)} · ${info.slot}`);
134
87
  }
135
88
  /**
136
89
  * Which affordance positions are actually insertable at `nodeId` (EC-405 scope 3):
137
90
  * a position is offered only when it structurally exists AND at least one registry
138
- * component may land there (the SAME slot-rule filter the tree/DnD and the blocks
139
- * panel apply — {@link hasInsertableComponent} over the resolved `slotAllow`), so a
91
+ * component may land there (the SAME judge the tree/DnD, the blocks panel and the
92
+ * reducer apply — {@link hasInsertableComponent} over the resolved `allow`), so a
140
93
  * blocked spot shows no dead button.
141
94
  *
142
95
  * An EMPTY, insertable container surfaces ONLY `into` — the meaningful action there
@@ -146,17 +99,17 @@ export function resolveInsertContextAt(builder, nodeId, position, fieldAllow, st
146
99
  * child already carries its own edge affordances, so a filled container needs no
147
100
  * center "+").
148
101
  */
149
- export function insertablePositions(builder, nodeId, fieldAllow, studio = getStudioRegistry()) {
102
+ export function insertablePositions(builder, nodeId, studio = getStudioRegistry()) {
150
103
  // Empty insertable container → into only.
151
- const into = resolveInsertContextAt(builder, nodeId, 'into', fieldAllow, studio);
152
- if (into && (into.target.index ?? 0) === 0 && hasInsertableComponent(studio, into.slotAllow)) {
104
+ const into = resolveInsertContextAt(builder, nodeId, 'into', studio);
105
+ if (into && (into.target.index ?? 0) === 0 && hasInsertableComponent(studio, into.allow)) {
153
106
  return ['into'];
154
107
  }
155
108
  // Otherwise the sibling edges that accept something.
156
109
  const out = [];
157
110
  for (const position of ['before', 'after']) {
158
- const ctx = resolveInsertContextAt(builder, nodeId, position, fieldAllow, studio);
159
- if (ctx && hasInsertableComponent(studio, ctx.slotAllow))
111
+ const ctx = resolveInsertContextAt(builder, nodeId, position, studio);
112
+ if (ctx && hasInsertableComponent(studio, ctx.allow))
160
113
  out.push(position);
161
114
  }
162
115
  return out;