@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
@@ -2,11 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { Link, STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router';
3
3
  import { Check, ChevronDown, CircleAlert, CircleCheck, Copy, Eye, EyeOff, GitCompareArrows, History, Loader2, Lock, MoreHorizontal, RotateCcw, Save, Tag, Trash2, X, } from 'lucide-react';
4
4
  import { useCallback, useMemo, useState } from 'react';
5
- import { FIELD_WIDTH_SPANS, SIDEBAR_FIELD_GROUP, documentKey, groupOrder, groupSectionOf, hasAliases, isBlocksField, isFieldHiddenIn, isFieldLocalized, isSingleton, localeViews, resolveDocumentTitle, resolveField, semanticValueEqual, sidebarFields, tabDescriptorOf, tabOrder, titleFieldOf, staticFieldHidden, valuesInLocale, writeField, } from '@elytracms/core/cms-core';
6
- import { blockRootsOf } from '@elytracms/core/project-graph';
5
+ import { DRAFT_PERSPECTIVE, DRAFT_VARIANT, FIELD_WIDTH_SPANS, SIDEBAR_FIELD_GROUP, documentKey, groupOrder, groupSectionOf, hasAliases, isBlocksField, isFieldHiddenIn, isFieldLocalized, isSingleton, localeViews, resolveDocumentTitle, resolveField, sidebarFields, tabDescriptorOf, tabOrder, titleFieldOf, staticFieldHidden, valuesInLocale, perspectiveKey, variantOf, } from '@elytracms/core/cms-core';
7
6
  import { Badge, Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, CollapsibleSection, Field, Heading, Row, Spacer, Stack, Text, toast, } from '@elytracms/studio/ui';
8
- import { getStudioRegistry, studioManifestLookup } from '@elytracms/studio/lib/registry';
9
- import { collectEmbedRefs, resolveReuseCapability } from '../builder/reuse.js';
7
+ import { studioManifestLookup } from '@elytracms/studio/lib/registry';
10
8
  import { cn } from '@elytracms/studio/lib/utils';
11
9
  import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
12
10
  import { LocaleSwitcher, displayedFieldFallback, fieldConnection, fillFieldFromBase, resetFieldToBase, } from '@elytracms/studio/lib/locale';
@@ -37,12 +35,15 @@ import { DetailsPane } from './details-pane.js';
37
35
  import { DocumentUrlPath } from './document-url-path.js';
38
36
  import { useSetBreadcrumb } from '../breadcrumb-context.js';
39
37
  import { useAiCurrentDocumentTap } from '@elytracms/studio/lib/ai-current-document';
40
- import { resolveDeploymentReuse, resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace';
38
+ import { resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace';
41
39
  // EC-325 — the controller hooks EditorBody orchestrates (each independently
42
40
  // unit-tested at its pure core); the shell threads shared draft/ref/replaceDraft
43
41
  // state across their boundaries.
44
42
  import { usePublishedPerspective } from './use-published-perspective.js';
45
- import { useSaveMode } from './use-save-mode.js';
43
+ import { useDocumentDraft } from './use-document-draft.js';
44
+ import { FieldCommitBoundary, FieldConflictNotice, FieldPresenceNote, conflictActorName, pathInField, presentInField, } from './field-collaboration.js';
45
+ import { fieldPositionPath } from '@elytracms/studio/lib/cms/field-patch';
46
+ import { getStudioActor } from '@elytracms/studio/lib/operations';
46
47
  import { useDiscardController } from './use-discard-controller.js';
47
48
  import { usePublishController, publishGate } from './use-publish-controller.js';
48
49
  import { FieldFocusRequestContext, FieldPathContext } from './field-anchors.js';
@@ -109,40 +110,44 @@ function EditorBody({ projectId, collection, doc, focusPath, formAppendix, }) {
109
110
  // to, and its one document is fixed (never deleted). The header drops the
110
111
  // back-crumb/id line and the Delete action accordingly.
111
112
  const singleton = isSingleton(collection);
112
- const [draft, setDraft] = useState(() => structuredClone(doc));
113
113
  // The studio's ambient active locale (EC-176): the form displays and edits
114
114
  // exactly this locale — switched via the shared LocaleSwitcher, never per field.
115
115
  const locale = source.activeLocale;
116
- // Bumped when a version restore replaces the draft out-of-band, to remount the
117
- // (uncontrolled) canvas editor from the restored value (EC-224).
118
- const [restoreNonce, setRestoreNonce] = useState(0);
119
116
  // EC-173: unpublish/delete go through the reference-aware confirmation.
120
117
  const [confirmAction, setConfirmAction] = useState(null);
121
118
  // Stable so the document-editor context value (EC-224) doesn't churn each render.
122
119
  const ref = useMemo(() => ({ collection: collection.id, id: doc.id }), [collection.id, doc.id]);
123
120
  const confirmTargets = useMemo(() => [{ collection: collection.id, id: doc.id }], [collection.id, doc.id]);
124
- // EC-224 — the working draft's two out-of-band mutators, shared across the
125
- // controller hooks below and the document-editor context. `save` persists the
126
- // current buffer; `replaceDraft` adopts a restored snapshot and bumps the nonce
127
- // to REMOUNT the uncontrolled canvas from it (else the next edit clobbers the
128
- // restore — the canvas builds its graph once at mount and lifts edits OUT).
129
- function save() {
130
- source.save(draft);
131
- }
132
- function replaceDraft(next) {
133
- setDraft(next);
134
- setRestoreNonce((n) => n + 1);
135
- }
136
- // EC-378 — the current-document seam: publish WHICH document is open (and the
137
- // draft-adoption channel) so the AI assistant resolves "this document" without
138
- // asking, and an applied update replaces this buffer instead of being clobbered
139
- // by the next autosave. A narrow read-only tap + the existing `replaceDraft`;
140
- // nothing else of the editor is exposed.
121
+ // EC-595 A2 — the draft over the revisioned document (`useDocumentDraft`): per-keystroke
122
+ // edits, committed per control kind (commit on quiescence) as the patches of the
123
+ // positions they wrote; rebased on the session's document whenever it changes (a write
124
+ // result, a colleague's change by subscription); forced flushes on unmount and tab
125
+ // hide; the thin presence heartbeat. The explicit-save rails keep their Save button.
126
+ // EC-188: the canvas field is found from the schema (its tab), so the draft knows it.
127
+ const lookupManifest = useMemo(() => studioManifestLookup(), []);
128
+ const draftCanvasField = collection.fields.find(isBlocksField);
129
+ const documentDraft = useDocumentDraft({
130
+ source,
131
+ collection,
132
+ doc,
133
+ locale,
134
+ lookupManifest,
135
+ ...(draftCanvasField ? { canvasField: draftCanvasField } : {}),
136
+ });
137
+ const { draft, dirty, explicitSave } = documentDraft;
138
+ // `save` — the forced commit (document model) / the Save button (record model);
139
+ // `replaceDraft` adopts a restored snapshot (the canvas remounts when its value moved).
140
+ const save = documentDraft.save;
141
+ const replaceDraft = documentDraft.replaceDraft;
142
+ // EC-378 — the current-document seam: publish WHICH document is open so the AI
143
+ // assistant resolves "this document" without asking, and the FORCED FLUSH it runs
144
+ // before reading or patching it (EC-595 A2 — the assistant's applied changes reach
145
+ // this editor through the session, which the draft rebases on).
141
146
  useAiCurrentDocumentTap({
142
147
  collection: collection.id,
143
148
  id: doc.id,
144
149
  title: primaryTitle(draft, collection, locale, source.localeConfig) || doc.id,
145
- adoptDraft: replaceDraft,
150
+ flush: documentDraft.flush,
146
151
  });
147
152
  // EC-226 / EC-316 / EC-325 — the Draft/Published VIEW perspective AND the pinned
148
153
  // published snapshot, both derived from the ONE shared version-history query
@@ -150,25 +155,21 @@ function EditorBody({ projectId, collection, doc, focusPath, formAppendix, }) {
150
155
  // in this component). `canDiscard`/`versionKey`/`publishedVersionNumber`/
151
156
  // `publishedSnapshotDoc` feed the Discard controller below off that same query.
152
157
  const { perspective, setPerspective, published, canDiscard, versionKey, publishedVersionNumber, publishedSnapshotDoc, publishedSnapshot, viewReadOnly, viewDoc, publishedPending, } = usePublishedPerspective({ ref, source, canPublish, canVersions, draft });
153
- // EC-586: THE equality verdict (`semanticValueEqual` over canonical forms) — key
154
- // order is an artifact of how a value was built, never a change; the same verdict
155
- // the store's "has unpublished changes" reads.
156
- const dirty = useMemo(() => !semanticValueEqual(draft, doc), [draft, doc]);
157
- const schemaIssues = useMemo(() => source.validate(draft), [source, draft]);
158
- // EC-300/AD-16: composition embeds (hard references) whose target isn't published
159
- // would render empty on the live site. Surface them as errors in the Issues panel,
160
- // but they DON'T gate the Publish button (schema errors do) — publishing is
161
- // confirm-through, so the editor can publish the page and the reusable together
162
- // knowingly. The publish guard below catches them.
163
- const referenceIssues = useMemo(() => unpublishedEmbedIssues(collection, draft, source), [collection, draft, source]);
164
- const issues = useMemo(() => [...schemaIssues, ...referenceIssues], [schemaIssues, referenceIssues]);
165
- const errorCount = schemaIssues.filter((i) => i.severity === 'error').length;
158
+ // EC-586: THE equality verdict (`semanticValueEqual` over canonical forms) is the
159
+ // draft's `dirty` (`useDocumentDraft`): uncommitted positions exist.
160
+ // EC-589: ONE validation path. A strict relation (the reuse embed declares
161
+ // `strict: true` on its relation prop) to an unpublished target is a blocking error
162
+ // from the validator's relation-target pass — at every position the reference walk
163
+ // reaches, localized buckets and rich-text embeds included — so it gates the Publish
164
+ // button like any other error, and `documents.publish` refuses it for every client.
165
+ // (The EC-300 special case that walked top-level `blocks` in `values` only, and let
166
+ // the editor publish through, is gone.)
167
+ const issues = useMemo(() => source.validate(draft), [source, draft]);
168
+ const errorCount = issues.filter((i) => i.severity === 'error').length;
166
169
  const hasErrors = errorCount > 0;
167
- const hasUnpublishedRefs = referenceIssues.length > 0;
168
- // EC-225 autosave · EC-240 explicit save — the persistence discipline for this
169
- // rail (a publishing rail autosaves + flushes on unmount; a non-publishing rail
170
- // saves explicitly and relies on the `UnsavedChangesGuard` below for navigation).
171
- const { explicitSave } = useSaveMode({ canPublish, dirty, draft, source });
170
+ // EC-595 A2 · EC-240 — the write model decides the discipline (`source.writes`): a
171
+ // document commits on quiescence and flushes on unmount / tab hide; a record saves
172
+ // explicitly and relies on the `UnsavedChangesGuard` below for navigation.
172
173
  // EC-316 / EC-325 — the "Discard changes" controller: the pure enablement
173
174
  // decision plus the shared restore sequence (flush → restore the pinned published
174
175
  // version whole → replaceDraft → invalidate). Destructured to the names the header
@@ -277,9 +278,22 @@ function EditorBody({ projectId, collection, doc, focusPath, formAppendix, }) {
277
278
  // reuse the same foundation, so typing, filling, and resetting can never diverge.
278
279
  // EC-586: the write canonicalises (`writeField`), and the manifest lookup covers a
279
280
  // composition's static props — a canvas or embed edit lands canonical in the draft.
280
- const lookupManifest = useMemo(() => studioManifestLookup(), []);
281
+ // EC-595 A2: each write is also the patch of the position it wrote, committed per the
282
+ // field's control kind (`useDocumentDraft` → `commit-policy.ts`).
281
283
  function setValue(field, value) {
282
- setDraft((d) => writeField(collection, d, field.name, locale, value, { lookupManifest }));
284
+ documentDraft.setFieldValue(field, value);
285
+ }
286
+ // EC-595 A2 — the per-field conflicts and the thin presence on this document.
287
+ const documentWrites = source.writes.kind === 'document' ? source.writes : undefined;
288
+ const conflicts = documentWrites?.conflicts(ref) ?? [];
289
+ const selfActor = getStudioActor()?.id;
290
+ function conflictNotices(field) {
291
+ if (!documentWrites)
292
+ return null;
293
+ const own = conflicts.filter((conflict) => pathInField(conflict.path, field.name, isFieldLocalized(collection, field.name) ? locale : undefined));
294
+ if (own.length === 0)
295
+ return null;
296
+ return (_jsx(Stack, { gap: "1", children: own.map((conflict) => (_jsx(FieldConflictNotice, { conflict: conflict, actorName: conflictActorName(conflict, documentDraft.presence, selfActor), onTakeTheirs: () => documentWrites.resolveConflict(ref, conflict.id, 'theirs'), onKeepMine: () => documentWrites.resolveConflict(ref, conflict.id, 'mine') }, conflict.id))) }));
283
297
  }
284
298
  // EC-273/EC-580: conditional visibility, judged in the DISPLAYED view of the
285
299
  // draft (`valuesInLocale`, with the default-locale fallback) — a localized field
@@ -322,7 +336,7 @@ function EditorBody({ projectId, collection, doc, focusPath, formAppendix, }) {
322
336
  const connection = localized
323
337
  ? fieldConnection(collection, draft, field.name, locale, source.localeConfig)
324
338
  : null;
325
- return (_jsx(Field, { className: cn('rounded-md transition-shadow', highlightedField === field.name && 'bg-primary/5 p-2 ring-2 ring-primary/70'), label: _jsxs(Row, { gap: "2", children: [_jsx("span", { children: fieldLabel(field) }), field.validation?.required ? _jsx("span", { className: "text-destructive", children: "*" }) : null, localized ? _jsx(Badge, { variant: "outline", children: locale }) : null, fieldLevelReadOnly ? (_jsxs(Badge, { variant: "outline", children: [_jsx(Lock, { className: "size-3" }), " ", translate('cms.editor.readOnlyBadge')] })) : null, _jsx(Spacer, {}), connection && !fieldReadOnly ? (_jsx(FieldConnectionControl, { connection: connection, fieldName: field.name, locale: locale, onFillFromBase: () => setDraft((d) => fillFieldFromBase(collection, d, field.name, locale, source.localeConfig)), onResetToBase: () => setDraft((d) => resetFieldToBase(collection, d, field.name, locale)) })) : null] }), htmlFor: `f-${field.name}`, fieldPath: field.name, hint: localizedLabel(field.form?.description) || undefined, error: fieldReadOnly ? undefined : errorsByField.get(field.name), children: _jsx(FieldPathContext.Provider, { value: field.name, children: _jsx(FieldControl, { id: `f-${field.name}`, field: field, value: getValue(field), onChange: (v) => setValue(field, v), readOnly: fieldReadOnly }) }) }, field.name));
339
+ return (_jsxs(Field, { className: cn('rounded-md transition-shadow', highlightedField === field.name && 'bg-primary/5 p-2 ring-2 ring-primary/70'), label: _jsxs(Row, { gap: "2", children: [_jsx("span", { children: fieldLabel(field) }), field.validation?.required ? _jsx("span", { className: "text-destructive", children: "*" }) : null, localized ? _jsx(Badge, { variant: "outline", children: locale }) : null, fieldLevelReadOnly ? (_jsxs(Badge, { variant: "outline", children: [_jsx(Lock, { className: "size-3" }), " ", translate('cms.editor.readOnlyBadge')] })) : null, _jsx(Spacer, {}), connection && !fieldReadOnly ? (_jsx(FieldConnectionControl, { connection: connection, fieldName: field.name, locale: locale, onFillFromBase: () => documentDraft.editField(field, (d) => fillFieldFromBase(collection, d, field.name, locale, source.localeConfig)), onResetToBase: () => documentDraft.editField(field, (d) => resetFieldToBase(collection, d, field.name, locale)) })) : null] }), htmlFor: `f-${field.name}`, fieldPath: field.name, hint: localizedLabel(field.form?.description) || undefined, error: fieldReadOnly ? undefined : errorsByField.get(field.name), children: [_jsx(FieldPresenceNote, { entries: presentInField(documentDraft.presence, fieldPositionPath(collection, field.name, locale)) }), fieldReadOnly ? null : conflictNotices(field), _jsx(FieldCommitBoundary, { onFocusField: () => documentDraft.focused(field), onBlurField: () => documentDraft.blurred(field), onEnter: () => documentDraft.entered(field), children: _jsx(FieldPathContext.Provider, { value: field.name, children: _jsx(FieldControl, { id: `f-${field.name}`, field: field, value: getValue(field), onChange: (v) => setValue(field, v), readOnly: fieldReadOnly }) }) })] }, field.name));
326
340
  }
327
341
  // The frame-owned `Versions` pane (EC-224) — appended after any view-supplied
328
342
  // pane (the canvas's `Properties`). One instance feeds both the form view's
@@ -367,14 +381,7 @@ function EditorBody({ projectId, collection, doc, focusPath, formAppendix, }) {
367
381
  id: 'details',
368
382
  label: translate('cms.editor.detailsPane'),
369
383
  icon: _jsx(Tag, { className: "size-3.5" }),
370
- render: () => (_jsx(DetailsPane, { collection: collection, doc: draft, siblings: source.documents(), readOnly: viewReadOnly, fields: visibleSidebarFields, renderField: renderFieldRow, onAliasChange: (alias) => setDraft((d) => {
371
- const next = { ...d };
372
- if (alias === undefined)
373
- delete next.alias;
374
- else
375
- next.alias = alias;
376
- return next;
377
- }) })),
384
+ render: () => (_jsx(DetailsPane, { collection: collection, doc: draft, siblings: source.documents(), readOnly: viewReadOnly, fields: visibleSidebarFields, renderField: renderFieldRow, onAliasChange: (alias) => documentDraft.setAlias(alias) })),
378
385
  }
379
386
  : null;
380
387
  // Versions is capability-gated (AD-19): a non-versioned rail (users) never mounts
@@ -384,19 +391,13 @@ function EditorBody({ projectId, collection, doc, focusPath, formAppendix, }) {
384
391
  ...(canVersions ? [versionsPane] : []),
385
392
  issuesPane,
386
393
  ];
387
- // EC-268 / EC-300 / 0.0.39 — the publish controller: the publish indicator state,
388
- // the EC-300 confirm-through guard (unpublished hard refs would render empty on
389
- // the live site), and the "Publish & close" navigation. Handlers are named to
390
- // match the header split button + the confirm-through dialog below.
391
- const { publishState, publishConfirm, handlePublish, handlePublishAndClose, confirmPublish, cancelPublishConfirm, } = usePublishController({
394
+ // EC-268 / 0.0.39 — the publish controller: the publish indicator state and the
395
+ // "Publish & close" navigation. Handlers are named to match the header split button.
396
+ const { publishState, handlePublish, handlePublishAndClose } = usePublishController({
392
397
  source,
393
398
  docRef: ref,
394
- collection,
395
399
  projectId,
396
- draft,
397
- setDraft,
398
- dirty,
399
- hasUnpublishedRefs,
400
+ flushDraft: documentDraft.flush,
400
401
  });
401
402
  // EC-442 (amended): Duplicate in the header `…` menu — gated on the explicit
402
403
  // `duplicate` capability (CMS collections only, owner call 2026-07-17) + a
@@ -404,12 +405,15 @@ function EditorBody({ projectId, collection, doc, focusPath, formAppendix, }) {
404
405
  // document"; a copy would be EC-274 drift by construction, mirroring the
405
406
  // hidden Delete).
406
407
  const canDuplicate = !singleton && source.capabilities.duplicate && source.detailPath !== undefined;
407
- function handleDuplicate() {
408
- // Copy the CURRENT draft buffer — what the editor sees — not the possibly
409
- // autosave-lagged stored row. The duplicate is a fresh DRAFT, never
410
- // auto-published (live-is-published-only), created through the seam's
411
- // existing create+save write paths (`duplicateFromSource`).
412
- const newRef = duplicateFromSource(source, draft);
408
+ async function handleDuplicate() {
409
+ // EC-595 A2: the FORCED FLUSH first — the uncommitted fields commit and land — then
410
+ // copy the committed draft (what the editor sees). The duplicate is a fresh DRAFT,
411
+ // never auto-published (live-is-published-only), created insert-if-absent with a
412
+ // factory id (`duplicateFromSource`).
413
+ await documentDraft.flush();
414
+ const newRef = duplicateFromSource(source, source.get(ref) ?? draft);
415
+ if (!newRef)
416
+ return;
413
417
  toast.success(translate('cms.editor.duplicateCreated'));
414
418
  const splat = source.detailPath?.(newRef.id);
415
419
  if (splat === undefined)
@@ -512,20 +516,13 @@ function EditorBody({ projectId, collection, doc, focusPath, formAppendix, }) {
512
516
  exitCompare,
513
517
  }, children: _jsx(FieldFocusRequestContext.Provider, { value: focusRequest, children: _jsxs("div", { className: cn('flex h-full min-h-0 flex-col', viewReadOnly && 'bg-info/[0.04]'), children: [_jsxs("div", { className: "shrink-0 border-b border-border px-4 py-3 sm:px-6", children: [_jsxs(Row, { gap: "3", align: "start", children: [_jsxs(Stack, { gap: "1", className: "min-w-0", children: [_jsxs(Row, { gap: "2", align: "center", className: "min-w-0", children: [_jsx(Heading, { level: 1, size: "h2", className: "truncate", children: singleton ? localizedLabel(collection.form?.label, collection.id) : title || doc.id }), singleton ? null : _jsx(FallbackChip, { fallback: titleFallback, testId: "doc-title-fallback" })] }), routableCollection ? (_jsx(DocumentUrlPath, { projectId: projectId, collection: collection, doc: draft, siblings: source.documents(), routes: resolveDeploymentRoutes(), localeConfig: source.localeConfig, locale: locale, live: headerLive })) : null] }), _jsx(Spacer, {}), _jsxs(Stack, { gap: "none", align: "end", className: "min-w-0", children: [_jsx(Text, { size: "xs", tone: "muted", className: "whitespace-nowrap", "data-testid": "autosave-status", children: source.lastError
514
518
  ? translate('cms.editor.saveFailed')
515
- : explicitSave
516
- ? dirty
517
- ? translate('cms.editor.unsavedChanges')
518
- : translate('cms.editor.saved')
519
- : dirty
520
- ? translate('cms.editor.saving')
521
- : translate('cms.editor.saved') }), source.lastError?.message ? (_jsx(Text, { size: "xs", className: "max-w-64 truncate text-destructive", title: source.lastError.message, "data-testid": "save-error-message", children: source.lastError.message })) : null] }), canPublish ? _jsx(DocStateBadge, { published: published }) : null, canPublish ? (_jsxs(Row, { gap: "px", align: "center", children: [_jsx(Button, { size: "sm", onClick: handlePublish, disabled: publishDisabled, title: publishTitle, className: canPublishClose ? 'rounded-r-none' : undefined, "data-testid": "doc-publish", children: publishButtonLabel }), canPublishClose ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { size: "sm", disabled: publishDisabled, "aria-label": translate('cms.editor.publishActions'), "data-testid": "doc-publish-actions", className: "rounded-l-none border-l border-primary-foreground/25 px-1.5", children: _jsx(ChevronDown, { className: "size-3.5" }) }) }), _jsx(DropdownMenuContent, { align: "end", children: _jsxs(DropdownMenuItem, { onClick: handlePublishAndClose, "data-testid": "doc-publish-and-close", children: [_jsx(Save, {}), " ", translate('cms.editor.publishAndClose')] }) })] })) : null] })) : null, explicitSave ? (_jsxs(Button, { size: "sm", onClick: save, disabled: !dirty, "data-testid": "doc-save", children: [_jsx(Check, {}), " ", translate('common.save')] })) : null, !viewReadOnly ? (_jsx(TranslateDocumentAction, { projectId: projectId, collection: collection, docRef: ref, document: draft, localeConfig: source.localeConfig, localesEnabled: canLocales, onApplied: (next) => {
522
- source.save(next);
523
- replaceDraft(next);
524
- } })) : null, published || !singleton ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "outline", size: "icon-sm", "aria-label": translate('cms.editor.moreActions'), children: _jsx(MoreHorizontal, {}) }) }), _jsxs(DropdownMenuContent, { align: "end", children: [canDuplicate ? (_jsxs(DropdownMenuItem, { "data-testid": "doc-duplicate", onClick: handleDuplicate, children: [_jsx(Copy, {}), " ", translate('common.duplicate')] })) : null, discardStatus !== 'hidden' ? (_jsxs(DropdownMenuItem, { "data-testid": "doc-discard-draft", disabled: discardStatus !== 'ready', title: discardDisabledReason, onClick: () => setDiscardConfirm(true), children: [_jsx(RotateCcw, {}), " ", translate('cms.editor.discardDraft')] })) : null, published ? (_jsxs(DropdownMenuItem, { onClick: () => setConfirmAction('unpublish'), children: [_jsx(EyeOff, {}), " ", translate('cms.editor.unpublish')] })) : null, singleton ? null : (_jsxs(DropdownMenuItem, { "data-testid": "doc-delete", className: "text-destructive", onClick: () => setConfirmAction('delete'), children: [_jsx(Trash2, {}), " ", translate('common.delete')] }))] })] })) : null] }), showLensRow ? (_jsxs(Row, { gap: "3", className: "mt-3", wrap: true, children: [_jsxs(Row, { gap: "1", align: "center", children: [showContentTabs ? tabs.map((tab) => renderContentTab(tab)) : null, showReferences ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "mx-1 h-4 w-px bg-border", "aria-hidden": true }), renderContentTab(REFERENCES_TAB)] })) : null, compare ? (_jsxs("span", { className: cn('inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-medium transition-colors', activeTab === COMPARE_TAB
519
+ : dirty
520
+ ? translate('cms.editor.unsavedChanges')
521
+ : translate('cms.editor.saved') }), source.lastError?.message ? (_jsx(Text, { size: "xs", className: "max-w-64 truncate text-destructive", title: source.lastError.message, "data-testid": "save-error-message", children: source.lastError.message })) : null] }), canPublish ? _jsx(DocStateBadge, { published: published }) : null, canPublish ? (_jsxs(Row, { gap: "px", align: "center", children: [_jsx(Button, { size: "sm", onClick: handlePublish, disabled: publishDisabled, title: publishTitle, className: canPublishClose ? 'rounded-r-none' : undefined, "data-testid": "doc-publish", children: publishButtonLabel }), canPublishClose ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { size: "sm", disabled: publishDisabled, "aria-label": translate('cms.editor.publishActions'), "data-testid": "doc-publish-actions", className: "rounded-l-none border-l border-primary-foreground/25 px-1.5", children: _jsx(ChevronDown, { className: "size-3.5" }) }) }), _jsx(DropdownMenuContent, { align: "end", children: _jsxs(DropdownMenuItem, { onClick: handlePublishAndClose, "data-testid": "doc-publish-and-close", children: [_jsx(Save, {}), " ", translate('cms.editor.publishAndClose')] }) })] })) : null] })) : null, explicitSave ? (_jsxs(Button, { size: "sm", onClick: save, disabled: !dirty, "data-testid": "doc-save", children: [_jsx(Check, {}), " ", translate('common.save')] })) : null, !viewReadOnly ? (_jsx(TranslateDocumentAction, { projectId: projectId, collection: collection, docRef: ref, document: draft, localeConfig: source.localeConfig, localesEnabled: canLocales, flushDraft: documentDraft.flush })) : null, published || !singleton ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "outline", size: "icon-sm", "aria-label": translate('cms.editor.moreActions'), children: _jsx(MoreHorizontal, {}) }) }), _jsxs(DropdownMenuContent, { align: "end", children: [canDuplicate ? (_jsxs(DropdownMenuItem, { "data-testid": "doc-duplicate", onClick: () => void handleDuplicate(), children: [_jsx(Copy, {}), " ", translate('common.duplicate')] })) : null, discardStatus !== 'hidden' ? (_jsxs(DropdownMenuItem, { "data-testid": "doc-discard-draft", disabled: discardStatus !== 'ready', title: discardDisabledReason, onClick: () => setDiscardConfirm(true), children: [_jsx(RotateCcw, {}), " ", translate('cms.editor.discardDraft')] })) : null, published ? (_jsxs(DropdownMenuItem, { onClick: () => setConfirmAction('unpublish'), children: [_jsx(EyeOff, {}), " ", translate('cms.editor.unpublish')] })) : null, singleton ? null : (_jsxs(DropdownMenuItem, { "data-testid": "doc-delete", className: "text-destructive", onClick: () => setConfirmAction('delete'), children: [_jsx(Trash2, {}), " ", translate('common.delete')] }))] })] })) : null] }), showLensRow ? (_jsxs(Row, { gap: "3", className: "mt-3", wrap: true, children: [_jsxs(Row, { gap: "1", align: "center", children: [showContentTabs ? tabs.map((tab) => renderContentTab(tab)) : null, showReferences ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "mx-1 h-4 w-px bg-border", "aria-hidden": true }), renderContentTab(REFERENCES_TAB)] })) : null, compare ? (_jsxs("span", { className: cn('inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-medium transition-colors', activeTab === COMPARE_TAB
525
522
  ? 'bg-accent text-accent-foreground'
526
- : 'text-muted-foreground hover:bg-muted'), children: [_jsxs("button", { type: "button", onClick: () => setActiveTab(COMPARE_TAB), "aria-pressed": activeTab === COMPARE_TAB, className: "inline-flex items-center gap-1", children: [_jsx(GitCompareArrows, { className: "size-3", "aria-hidden": true }), " ", translate('cms.version.compare')] }), _jsx("button", { type: "button", onClick: exitCompare, "aria-label": translate('cms.version.closeCompare'), className: "ml-0.5 rounded p-0.5 hover:bg-background/40", children: _jsx(X, { className: "size-3" }) })] })) : null] }), _jsx(Spacer, {}), canLocales ? _jsx(LocaleSwitcher, {}) : null, canPublish ? (_jsxs(Row, { gap: "px", className: "rounded-md bg-muted/60 p-0.5", children: [_jsx(PerspectiveButton, { label: translate('cms.editor.draftState'), active: perspective === 'draft', onClick: () => setPerspective('draft') }), _jsx(PerspectiveButton, { label: translate('cms.editor.publishedState'), active: perspective === 'published', accent: "info", disabled: !published, title: published ? undefined : translate('cms.editor.publishToViewPublished'), onClick: () => setPerspective('published') })] })) : null] })) : null] }), viewReadOnly ? (_jsxs(Row, { gap: "2", className: "shrink-0 border-b border-info/30 bg-info/10 px-4 py-1.5 text-info sm:px-6", "data-testid": "published-view-banner", children: [_jsx(Lock, { className: "size-3.5 shrink-0", "aria-hidden": true }), _jsx(Text, { size: "xs", className: "text-info", children: translate('cms.editor.publishedReadOnlyBanner') }), _jsx(Spacer, {}), _jsx("button", { type: "button", onClick: () => setPerspective('draft'), className: "text-xs font-medium underline-offset-2 hover:underline", children: translate('cms.editor.switchToDraft') })] })) : null, publishedPending ? (_jsx("div", { className: "grid min-h-0 flex-1 place-items-center p-8", children: _jsx(Text, { size: "sm", tone: "muted", children: publishedSnapshot.status === 'loading'
523
+ : 'text-muted-foreground hover:bg-muted'), children: [_jsxs("button", { type: "button", onClick: () => setActiveTab(COMPARE_TAB), "aria-pressed": activeTab === COMPARE_TAB, className: "inline-flex items-center gap-1", children: [_jsx(GitCompareArrows, { className: "size-3", "aria-hidden": true }), " ", translate('cms.version.compare')] }), _jsx("button", { type: "button", onClick: exitCompare, "aria-label": translate('cms.version.closeCompare'), className: "ml-0.5 rounded p-0.5 hover:bg-background/40", children: _jsx(X, { className: "size-3" }) })] })) : null] }), _jsx(Spacer, {}), canLocales ? _jsx(LocaleSwitcher, {}) : null, canPublish ? (_jsxs(Row, { gap: "px", className: "rounded-md bg-muted/60 p-0.5", children: [_jsx(PerspectiveButton, { label: translate('cms.editor.draftState'), active: variantOf(perspective) === DRAFT_VARIANT, onClick: () => setPerspective(DRAFT_PERSPECTIVE) }), _jsx(PerspectiveButton, { label: translate('cms.editor.publishedState'), active: perspective === 'published', accent: "info", disabled: !published, title: published ? undefined : translate('cms.editor.publishToViewPublished'), onClick: () => setPerspective('published') })] })) : null] })) : null] }), viewReadOnly ? (_jsxs(Row, { gap: "2", className: "shrink-0 border-b border-info/30 bg-info/10 px-4 py-1.5 text-info sm:px-6", "data-testid": "published-view-banner", children: [_jsx(Lock, { className: "size-3.5 shrink-0", "aria-hidden": true }), _jsx(Text, { size: "xs", className: "text-info", children: translate('cms.editor.publishedReadOnlyBanner') }), _jsx(Spacer, {}), _jsx("button", { type: "button", onClick: () => setPerspective(DRAFT_PERSPECTIVE), className: "text-xs font-medium underline-offset-2 hover:underline", children: translate('cms.editor.switchToDraft') })] })) : null, publishedPending ? (_jsx("div", { className: "grid min-h-0 flex-1 place-items-center p-8", children: _jsx(Text, { size: "sm", tone: "muted", children: publishedSnapshot.status === 'loading'
527
524
  ? translate('cms.editor.loadingPublishedVersion')
528
- : translate('cms.editor.noPublishedVersion') }) })) : activeTab === COMPARE_TAB && compare ? (_jsxs("div", { className: "flex min-h-0 flex-1 overflow-hidden", children: [_jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: _jsx(VersionCompare, {}) }), _jsx(InspectorSidebar, { panes: trailingPanes, activeId: inspectorPaneId, onActiveChange: setInspectorPaneId, className: "w-[320px] shrink-0 border-l border-border" })] })) : canvasField ? (_jsx("div", { className: "min-h-0 flex-1 overflow-hidden", children: _jsx(CompositionFieldEditor, { field: canvasField, value: getValue(canvasField), onChange: (v) => setValue(canvasField, v), readOnly: viewReadOnly || source.isFieldReadOnly(canvasField), trailingPanes: trailingPanes }, `${canvasField.name}:${restoreNonce}:${perspective}`) })) : activeTab === REFERENCES_TAB ? (_jsx("div", { className: "flex min-h-0 flex-1 overflow-hidden", children: _jsx("div", { className: "min-h-0 flex-1 overflow-auto p-4 sm:p-6", children: _jsx("div", { className: "mx-auto w-full max-w-2xl", children: _jsx(ReferencesPanel, { projectId: projectId, source: { type: 'document', id: documentKey(ref) }, target: { type: 'document', id: documentKey(ref) } }) }) }) })) : (_jsxs("div", { className: "flex min-h-0 flex-1 overflow-hidden", children: [_jsx("div", { className: "min-h-0 flex-1 overflow-auto p-4 sm:p-6", children: _jsx("div", { className: "mx-auto w-full max-w-2xl", children: _jsxs(Stack, { gap: "6", children: [groups.map((group) => {
525
+ : translate('cms.editor.noPublishedVersion') }) })) : activeTab === COMPARE_TAB && compare ? (_jsxs("div", { className: "flex min-h-0 flex-1 overflow-hidden", children: [_jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: _jsx(VersionCompare, {}) }), _jsx(InspectorSidebar, { panes: trailingPanes, activeId: inspectorPaneId, onActiveChange: setInspectorPaneId, className: "w-[320px] shrink-0 border-l border-border" })] })) : canvasField ? (_jsx("div", { className: "min-h-0 flex-1 overflow-hidden", children: _jsx(CompositionFieldEditor, { field: canvasField, value: getValue(canvasField), onChange: (v) => documentDraft.setCanvasValue(canvasField, v), conflicts: conflictNotices(canvasField), readOnly: viewReadOnly || source.isFieldReadOnly(canvasField), trailingPanes: trailingPanes }, `${canvasField.name}:${documentDraft.canvasNonce}:${perspectiveKey(perspective)}`) })) : activeTab === REFERENCES_TAB ? (_jsx("div", { className: "flex min-h-0 flex-1 overflow-hidden", children: _jsx("div", { className: "min-h-0 flex-1 overflow-auto p-4 sm:p-6", children: _jsx("div", { className: "mx-auto w-full max-w-2xl", children: _jsx(ReferencesPanel, { projectId: projectId, source: { type: 'document', id: documentKey(ref) }, target: { type: 'document', id: documentKey(ref) } }) }) }) })) : (_jsxs("div", { className: "flex min-h-0 flex-1 overflow-hidden", children: [_jsx("div", { className: "min-h-0 flex-1 overflow-auto p-4 sm:p-6", children: _jsx("div", { className: "mx-auto w-full max-w-2xl", children: _jsxs(Stack, { gap: "6", children: [groups.map((group) => {
529
526
  // EC-273/EC-240/EC-306: drop a fieldset whose every field is hidden —
530
527
  // by its own rule, by its (static or predicate) group section, or for
531
528
  // this principal — so the form never shows an empty section header.
@@ -547,58 +544,7 @@ function EditorBody({ projectId, collection, doc, focusPath, formAppendix, }) {
547
544
  if (!open)
548
545
  setConfirmAction(null);
549
546
  }, onConfirm: handleConfirmedAction })) : null, _jsx(Dialog, { open: discardConfirm, onOpenChange: (open) => { if (!open)
550
- setDiscardConfirm(false); }, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: translate('cms.editor.discardDraftTitle') }), _jsx(DialogDescription, { children: translate('cms.editor.discardDraftDescription') })] }), _jsxs(DialogFooter, { className: "mt-4", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => setDiscardConfirm(false), disabled: discardBusy, children: translate('common.cancel') }), _jsx(Button, { variant: "destructive", size: "sm", "data-testid": "discard-draft-confirm", disabled: discardBusy, onClick: () => void handleDiscardDraft(), children: discardBusy ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "animate-spin" }), " ", translate('cms.editor.discardingDraft')] })) : (_jsxs(_Fragment, { children: [_jsx(RotateCcw, {}), " ", translate('cms.editor.discardDraftConfirm')] })) })] })] }) }), _jsx(Dialog, { open: publishConfirm, onOpenChange: (open) => { if (!open)
551
- cancelPublishConfirm(); }, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: translate('cms.editor.publishUnpublishedRefsTitle') }), _jsx(DialogDescription, { children: translate('cms.editor.publishUnpublishedRefsDescription') })] }), _jsxs(Stack, { gap: "1", children: [referenceIssues.map((issue, i) => (_jsxs(Text, { size: "xs", tone: "muted", children: ["\u2022 ", cmsValidationIssueMessage(issue)] }, `${issue.meta?.id ?? i}`))), schemaIssues.length > 0 ? (_jsx(Text, { size: "xs", tone: "muted", className: "mt-1", children: translate(schemaIssues.length === 1
552
- ? 'cms.editor.publishAlsoHasIssues.one'
553
- : 'cms.editor.publishAlsoHasIssues.other', { count: schemaIssues.length }) })) : null] }), _jsxs(DialogFooter, { className: "mt-4", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: cancelPublishConfirm, children: translate('common.cancel') }), _jsx(Button, { size: "sm", "data-testid": "publish-anyway", onClick: confirmPublish, children: translate('cms.editor.publishAnyway') })] })] }) })] }) }) }));
554
- }
555
- /**
556
- * EC-300/AD-16 — errors for composition embeds (hard references) whose target isn't
557
- * published: they render empty on the live site. Walks every `blocks` field of the
558
- * document for reuse embeds; a missing target is an `unknown-relation-target` error,
559
- * an unpublished one an `unpublished-relation-target` error. Publishing is
560
- * confirm-through (not blocked), so these feed the Issues panel + the publish
561
- * confirmation, never the button's disabled gate. No reuse capability → none.
562
- *
563
- * EC-467: the capability honours the optional `reuse` studio-config declaration
564
- * (absent → the same inference as before). Config-level reuse ISSUES belong to the
565
- * shell rail, not to this document-scoped list — so only the capability is read here.
566
- */
567
- function unpublishedEmbedIssues(collection, draft, source) {
568
- const capability = resolveReuseCapability(getStudioRegistry().registry.list(), source.collections, resolveDeploymentReuse());
569
- if (!capability)
570
- return [];
571
- const issues = [];
572
- const seen = new Set();
573
- for (const field of collection.fields) {
574
- if (!isBlocksField(field))
575
- continue;
576
- for (const ref of collectEmbedRefs(blockRootsOf(draft.values?.[field.name]), capability)) {
577
- const key = `${ref.collection}:${ref.id}`;
578
- if (seen.has(key))
579
- continue;
580
- seen.add(key);
581
- const exists = source.get(ref) !== undefined;
582
- if (exists && (source.publishing?.isPublished(ref) ?? false))
583
- continue;
584
- issues.push(unpublishedEmbedIssue(collection, draft, field.name, ref, exists, source));
585
- }
586
- }
587
- return issues;
588
- }
589
- /** One unpublished/missing reuse-embed issue (EC-300). */
590
- function unpublishedEmbedIssue(collection, draft, field, ref, exists, source) {
591
- return {
592
- code: exists ? 'unpublished-relation-target' : 'unknown-relation-target',
593
- severity: 'error',
594
- message: exists
595
- ? translate('cms.editor.embedUnpublishedIssue', { title: source.titleOf(ref) })
596
- : translate('cms.editor.embedMissingIssue', { id: ref.id }),
597
- path: [field],
598
- collectionId: collection.id,
599
- documentId: draft.id,
600
- meta: { field, target: ref.collection, id: ref.id },
601
- };
547
+ setDiscardConfirm(false); }, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: translate('cms.editor.discardDraftTitle') }), _jsx(DialogDescription, { children: translate('cms.editor.discardDraftDescription') })] }), _jsxs(DialogFooter, { className: "mt-4", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => setDiscardConfirm(false), disabled: discardBusy, children: translate('common.cancel') }), _jsx(Button, { variant: "destructive", size: "sm", "data-testid": "discard-draft-confirm", disabled: discardBusy, onClick: () => void handleDiscardDraft(), children: discardBusy ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "animate-spin" }), " ", translate('cms.editor.discardingDraft')] })) : (_jsxs(_Fragment, { children: [_jsx(RotateCcw, {}), " ", translate('cms.editor.discardDraftConfirm')] })) })] })] }) })] }) }) }));
602
548
  }
603
549
  /**
604
550
  * A segmented-control button for the Draft/Published view toggle (EC-226). The