@pantheon-systems/puck-css 0.12.0 → 0.13.0

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 (466) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/components/AgentActionButton.js +0 -13
  3. package/dist/agent/components/AgentActionButton.js.map +1 -1
  4. package/dist/agent/components/AgentActionModal.js +0 -13
  5. package/dist/agent/components/AgentActionModal.js.map +1 -1
  6. package/dist/agent/components/AgentStatusPanel.js +0 -10
  7. package/dist/agent/components/AgentStatusPanel.js.map +1 -1
  8. package/dist/agent/components/index.js +0 -5
  9. package/dist/agent/components/index.js.map +1 -1
  10. package/dist/agent/useAgentEdit.js +0 -12
  11. package/dist/agent/useAgentEdit.js.map +1 -1
  12. package/dist/agent/useAgentTrigger.js +0 -29
  13. package/dist/agent/useAgentTrigger.js.map +1 -1
  14. package/dist/auth/P1AuthProvider.d.ts +9 -2
  15. package/dist/auth/P1AuthProvider.d.ts.map +1 -1
  16. package/dist/auth/P1AuthProvider.js +21 -44
  17. package/dist/auth/P1AuthProvider.js.map +1 -1
  18. package/dist/auth/P1LoginPage.js +0 -18
  19. package/dist/auth/P1LoginPage.js.map +1 -1
  20. package/dist/auth/index.d.ts +2 -0
  21. package/dist/auth/index.d.ts.map +1 -1
  22. package/dist/auth/index.js +1 -9
  23. package/dist/auth/index.js.map +1 -1
  24. package/dist/auth/logout.d.ts +23 -0
  25. package/dist/auth/logout.d.ts.map +1 -0
  26. package/dist/auth/logout.js +14 -0
  27. package/dist/auth/logout.js.map +1 -0
  28. package/dist/auth/storage-keys.d.ts +5 -0
  29. package/dist/auth/storage-keys.d.ts.map +1 -0
  30. package/dist/auth/storage-keys.js +3 -0
  31. package/dist/auth/storage-keys.js.map +1 -0
  32. package/dist/collaboration/components/AgentActivityBanner.js +0 -15
  33. package/dist/collaboration/components/AgentActivityBanner.js.map +1 -1
  34. package/dist/collaboration/components/CollaboratorAvatars.js +0 -17
  35. package/dist/collaboration/components/CollaboratorAvatars.js.map +1 -1
  36. package/dist/collaboration/components/FocusRegionHighlight.js +0 -9
  37. package/dist/collaboration/components/FocusRegionHighlight.js.map +1 -1
  38. package/dist/collaboration/components/PresenceIndicator.js +0 -9
  39. package/dist/collaboration/components/PresenceIndicator.js.map +1 -1
  40. package/dist/collaboration/useBranchPresence.js +0 -36
  41. package/dist/collaboration/useBranchPresence.js.map +1 -1
  42. package/dist/collaboration/useFocusRegionReporting.js +0 -75
  43. package/dist/collaboration/useFocusRegionReporting.js.map +1 -1
  44. package/dist/collaboration/usePresence.js +0 -37
  45. package/dist/collaboration/usePresence.js.map +1 -1
  46. package/dist/collaboration/useSitePresence.js +0 -36
  47. package/dist/collaboration/useSitePresence.js.map +1 -1
  48. package/dist/collaboration/utils/avatarColor.js +0 -3
  49. package/dist/collaboration/utils/avatarColor.js.map +1 -1
  50. package/dist/collaboration/utils/focusHighlightConfig.js +0 -61
  51. package/dist/collaboration/utils/focusHighlightConfig.js.map +1 -1
  52. package/dist/collaboration/utils/focusRegionMap.js +1 -80
  53. package/dist/collaboration/utils/focusRegionMap.js.map +1 -1
  54. package/dist/collaboration/utils/initials.js +0 -14
  55. package/dist/collaboration/utils/initials.js.map +1 -1
  56. package/dist/collaboration/utils/presenceIdentity.js +0 -26
  57. package/dist/collaboration/utils/presenceIdentity.js.map +1 -1
  58. package/dist/core/FocusHighlightContext.js +0 -31
  59. package/dist/core/FocusHighlightContext.js.map +1 -1
  60. package/dist/core/NotificationContext.js +2 -67
  61. package/dist/core/NotificationContext.js.map +1 -1
  62. package/dist/core/P1PuckContext.js +0 -31
  63. package/dist/core/P1PuckContext.js.map +1 -1
  64. package/dist/core/PresenceContext.js +0 -18
  65. package/dist/core/PresenceContext.js.map +1 -1
  66. package/dist/core/config.js +0 -11
  67. package/dist/core/config.js.map +1 -1
  68. package/dist/core/featureConfig.js +0 -32
  69. package/dist/core/featureConfig.js.map +1 -1
  70. package/dist/core/types.d.ts +1 -1
  71. package/dist/core/types.js +0 -3
  72. package/dist/core/types.js.map +1 -1
  73. package/dist/core/utils/debounce.js +0 -18
  74. package/dist/core/utils/debounce.js.map +1 -1
  75. package/dist/core/utils/requestError.js +0 -5
  76. package/dist/core/utils/requestError.js.map +1 -1
  77. package/dist/core/utils/retry.js +0 -31
  78. package/dist/core/utils/retry.js.map +1 -1
  79. package/dist/core/utils/throttle.js +0 -24
  80. package/dist/core/utils/throttle.js.map +1 -1
  81. package/dist/data/cross-reference-resolve.js +0 -6
  82. package/dist/data/cross-reference-resolve.js.map +1 -1
  83. package/dist/data/cross-reference.js +0 -15
  84. package/dist/data/cross-reference.js.map +1 -1
  85. package/dist/data/css-queries/css-query-registry.d.ts +2 -2
  86. package/dist/data/css-queries/css-query-registry.js +1 -1
  87. package/dist/data/css-queries/css-query-registry.js.map +1 -1
  88. package/dist/data/dal/index.js +0 -21
  89. package/dist/data/dal/index.js.map +1 -1
  90. package/dist/data/dal/init.js +0 -40
  91. package/dist/data/dal/init.js.map +1 -1
  92. package/dist/data/dal/p1-store.d.ts.map +1 -1
  93. package/dist/data/dal/p1-store.js +11 -41
  94. package/dist/data/dal/p1-store.js.map +1 -1
  95. package/dist/data/data-list-block/create-data-list-block.js +0 -7
  96. package/dist/data/data-list-block/create-data-list-block.js.map +1 -1
  97. package/dist/data/data-list-block/utils.js +0 -1
  98. package/dist/data/data-list-block/utils.js.map +1 -1
  99. package/dist/data/fields/pds-field-helpers.js +1 -4
  100. package/dist/data/fields/pds-field-helpers.js.map +1 -1
  101. package/dist/data/fields/visible-when.js +0 -5
  102. package/dist/data/fields/visible-when.js.map +1 -1
  103. package/dist/data/fields.js +0 -12
  104. package/dist/data/fields.js.map +1 -1
  105. package/dist/data/get-page.js +0 -1
  106. package/dist/data/get-page.js.map +1 -1
  107. package/dist/data/page-editor-meta.js +0 -7
  108. package/dist/data/page-editor-meta.js.map +1 -1
  109. package/dist/data/page-store.js +0 -32
  110. package/dist/data/page-store.js.map +1 -1
  111. package/dist/data/page-structure.js +0 -3
  112. package/dist/data/page-structure.js.map +1 -1
  113. package/dist/data/paths.js +0 -16
  114. package/dist/data/paths.js.map +1 -1
  115. package/dist/data/remote-datasources/fetch-http-json.js +0 -5
  116. package/dist/data/remote-datasources/fetch-http-json.js.map +1 -1
  117. package/dist/data/remote-datasources/loader.js +0 -21
  118. package/dist/data/remote-datasources/loader.js.map +1 -1
  119. package/dist/data/remote-datasources/remote-datasource-registry.js +0 -4
  120. package/dist/data/remote-datasources/remote-datasource-registry.js.map +1 -1
  121. package/dist/data/resolve-data-templates.js +0 -24
  122. package/dist/data/resolve-data-templates.js.map +1 -1
  123. package/dist/data/route-templates.js +0 -29
  124. package/dist/data/route-templates.js.map +1 -1
  125. package/dist/data/semantic-ops.js +0 -11
  126. package/dist/data/semantic-ops.js.map +1 -1
  127. package/dist/data/styles.js +0 -2
  128. package/dist/data/styles.js.map +1 -1
  129. package/dist/data/template-autocomplete.js +0 -8
  130. package/dist/data/template-autocomplete.js.map +1 -1
  131. package/dist/data/utils.d.ts +2 -0
  132. package/dist/data/utils.d.ts.map +1 -1
  133. package/dist/data/utils.js +3 -1
  134. package/dist/data/utils.js.map +1 -1
  135. package/dist/editor/P1App.d.ts.map +1 -1
  136. package/dist/editor/P1App.js +6 -45
  137. package/dist/editor/P1App.js.map +1 -1
  138. package/dist/editor/P1PuckProvider.d.ts +1 -1
  139. package/dist/editor/P1PuckProvider.js +39 -556
  140. package/dist/editor/P1PuckProvider.js.map +1 -1
  141. package/dist/editor/aiPanelStore.js +0 -7
  142. package/dist/editor/aiPanelStore.js.map +1 -1
  143. package/dist/editor/components/BranchSelector.js +0 -21
  144. package/dist/editor/components/BranchSelector.js.map +1 -1
  145. package/dist/editor/components/CollapsibleSectionHeader.js +1 -4
  146. package/dist/editor/components/CollapsibleSectionHeader.js.map +1 -1
  147. package/dist/editor/components/DraggableRow.js +0 -1
  148. package/dist/editor/components/DraggableRow.js.map +1 -1
  149. package/dist/editor/components/EditorReloadOverlay.d.ts +16 -0
  150. package/dist/editor/components/EditorReloadOverlay.d.ts.map +1 -0
  151. package/dist/editor/components/EditorReloadOverlay.js +13 -0
  152. package/dist/editor/components/EditorReloadOverlay.js.map +1 -0
  153. package/dist/editor/components/NotificationContainer.js +0 -20
  154. package/dist/editor/components/NotificationContainer.js.map +1 -1
  155. package/dist/editor/components/OutlinePanel.js +1 -12
  156. package/dist/editor/components/OutlinePanel.js.map +1 -1
  157. package/dist/editor/components/P1InspectorFields.d.ts.map +1 -1
  158. package/dist/editor/components/P1InspectorFields.js +12 -14
  159. package/dist/editor/components/P1InspectorFields.js.map +1 -1
  160. package/dist/editor/components/P1TemplateFields.js +6 -12
  161. package/dist/editor/components/P1TemplateFields.js.map +1 -1
  162. package/dist/editor/components/PanelHeader.d.ts.map +1 -1
  163. package/dist/editor/components/PanelHeader.js +3 -2
  164. package/dist/editor/components/PanelHeader.js.map +1 -1
  165. package/dist/editor/components/PublishButton.js +0 -21
  166. package/dist/editor/components/PublishButton.js.map +1 -1
  167. package/dist/editor/components/PuckDataCapture.js +0 -14
  168. package/dist/editor/components/PuckDataCapture.js.map +1 -1
  169. package/dist/editor/components/PuckDataSynchronizer.js +0 -67
  170. package/dist/editor/components/PuckDataSynchronizer.js.map +1 -1
  171. package/dist/editor/components/PuckSelectionTracker.js +0 -62
  172. package/dist/editor/components/PuckSelectionTracker.js.map +1 -1
  173. package/dist/editor/components/SaveIndicator.js +0 -41
  174. package/dist/editor/components/SaveIndicator.js.map +1 -1
  175. package/dist/editor/components/Toast.js +1 -32
  176. package/dist/editor/components/Toast.js.map +1 -1
  177. package/dist/editor/components/VersionBannerOverride.js +2 -14
  178. package/dist/editor/components/VersionBannerOverride.js.map +1 -1
  179. package/dist/editor/components/collapsibleSectionContext.js +0 -10
  180. package/dist/editor/components/collapsibleSectionContext.js.map +1 -1
  181. package/dist/editor/components/componentIconName.js +0 -23
  182. package/dist/editor/components/componentIconName.js.map +1 -1
  183. package/dist/editor/components/connectable.js +0 -5
  184. package/dist/editor/components/connectable.js.map +1 -1
  185. package/dist/editor/components/merge-preview/MergePreviewRenderer.js +2 -74
  186. package/dist/editor/components/merge-preview/MergePreviewRenderer.js.map +1 -1
  187. package/dist/editor/components/merge-preview/ScaledContent.js +0 -6
  188. package/dist/editor/components/merge-preview/ScaledContent.js.map +1 -1
  189. package/dist/editor/components/merge-preview/index.js +0 -5
  190. package/dist/editor/components/merge-preview/index.js.map +1 -1
  191. package/dist/editor/components/outlineTree.js +0 -31
  192. package/dist/editor/components/outlineTree.js.map +1 -1
  193. package/dist/editor/components/useDragReorder.js +0 -1
  194. package/dist/editor/components/useDragReorder.js.map +1 -1
  195. package/dist/editor/defaultPlugins.d.ts +1 -1
  196. package/dist/editor/defaultPlugins.js +1 -1
  197. package/dist/editor/logout-store.d.ts +13 -0
  198. package/dist/editor/logout-store.d.ts.map +1 -0
  199. package/dist/editor/logout-store.js +23 -0
  200. package/dist/editor/logout-store.js.map +1 -0
  201. package/dist/editor/plugin/P1Plugin.d.ts +6 -3
  202. package/dist/editor/plugin/P1Plugin.d.ts.map +1 -1
  203. package/dist/editor/plugin/P1Plugin.js +20 -170
  204. package/dist/editor/plugin/P1Plugin.js.map +1 -1
  205. package/dist/editor/plugin/context-sync-key.js +0 -19
  206. package/dist/editor/plugin/context-sync-key.js.map +1 -1
  207. package/dist/editor/plugin/createP1Overrides.js +1 -49
  208. package/dist/editor/plugin/createP1Overrides.js.map +1 -1
  209. package/dist/editor/plugin/document-sync-plugin.js +0 -50
  210. package/dist/editor/plugin/document-sync-plugin.js.map +1 -1
  211. package/dist/editor/plugin/fieldGuidance.js +0 -5
  212. package/dist/editor/plugin/fieldGuidance.js.map +1 -1
  213. package/dist/editor/plugin/index.js +0 -6
  214. package/dist/editor/plugin/index.js.map +1 -1
  215. package/dist/editor/plugin/selectHeaderCollaborators.js +0 -8
  216. package/dist/editor/plugin/selectHeaderCollaborators.js.map +1 -1
  217. package/dist/editor/presets.js +2 -2
  218. package/dist/editor/thumbnails/LiveThumbnail.js +1 -49
  219. package/dist/editor/thumbnails/LiveThumbnail.js.map +1 -1
  220. package/dist/editor/thumbnails/ThumbnailCard.js +0 -25
  221. package/dist/editor/thumbnails/ThumbnailCard.js.map +1 -1
  222. package/dist/editor/thumbnails/buildLiveThumbnailDrawer.js +0 -21
  223. package/dist/editor/thumbnails/buildLiveThumbnailDrawer.js.map +1 -1
  224. package/dist/editor/thumbnails/humanizeComponentName.js +0 -17
  225. package/dist/editor/thumbnails/humanizeComponentName.js.map +1 -1
  226. package/dist/editor/thumbnails/resolveLiveThumbnailDrawer.js +0 -11
  227. package/dist/editor/thumbnails/resolveLiveThumbnailDrawer.js.map +1 -1
  228. package/dist/editor/thumbnails/thumbnailCache.js +0 -38
  229. package/dist/editor/thumbnails/thumbnailCache.js.map +1 -1
  230. package/dist/editor/useAutoSave.js +0 -32
  231. package/dist/editor/useAutoSave.js.map +1 -1
  232. package/dist/editor/useBranches.js +0 -30
  233. package/dist/editor/useBranches.js.map +1 -1
  234. package/dist/editor/useComponentRegistry.d.ts +1 -1
  235. package/dist/editor/useComponentRegistry.js +0 -28
  236. package/dist/editor/useComponentRegistry.js.map +1 -1
  237. package/dist/editor/useDocuments.js +0 -51
  238. package/dist/editor/useDocuments.js.map +1 -1
  239. package/dist/editor/useMergePreview.js +8 -12
  240. package/dist/editor/useMergePreview.js.map +1 -1
  241. package/dist/editor/useP1Editor.d.ts +27 -7
  242. package/dist/editor/useP1Editor.d.ts.map +1 -1
  243. package/dist/editor/useP1Editor.js +106 -239
  244. package/dist/editor/useP1Editor.js.map +1 -1
  245. package/dist/editor/useP1Overrides.js +15 -50
  246. package/dist/editor/useP1Overrides.js.map +1 -1
  247. package/dist/editor/useP1Plugin.d.ts +7 -3
  248. package/dist/editor/useP1Plugin.d.ts.map +1 -1
  249. package/dist/editor/useP1Plugin.js +28 -56
  250. package/dist/editor/useP1Plugin.js.map +1 -1
  251. package/dist/editor/useRealtime.d.ts +15 -3
  252. package/dist/editor/useRealtime.d.ts.map +1 -1
  253. package/dist/editor/useRealtime.js +12 -91
  254. package/dist/editor/useRealtime.js.map +1 -1
  255. package/dist/editor/useResponsivePanels.js +2 -35
  256. package/dist/editor/useResponsivePanels.js.map +1 -1
  257. package/dist/editor/useSidebarScrollPreservation.js +0 -14
  258. package/dist/editor/useSidebarScrollPreservation.js.map +1 -1
  259. package/dist/editor/utils/buildThumbnailOverride.js +0 -9
  260. package/dist/editor/utils/buildThumbnailOverride.js.map +1 -1
  261. package/dist/editor/utils/componentRegistry.d.ts +6 -11
  262. package/dist/editor/utils/componentRegistry.d.ts.map +1 -1
  263. package/dist/editor/utils/componentRegistry.js +1 -71
  264. package/dist/editor/utils/componentRegistry.js.map +1 -1
  265. package/dist/editor/utils/createStablePluginArray.js +0 -27
  266. package/dist/editor/utils/createStablePluginArray.js.map +1 -1
  267. package/dist/editor/utils/path.js +0 -6
  268. package/dist/editor/utils/path.js.map +1 -1
  269. package/dist/editor/utils/puckYjsBinding.d.ts +1 -1
  270. package/dist/editor/utils/puckYjsBinding.js +0 -99
  271. package/dist/editor/utils/puckYjsBinding.js.map +1 -1
  272. package/dist/editor/utils/restoreDocumentVersion.js +0 -12
  273. package/dist/editor/utils/restoreDocumentVersion.js.map +1 -1
  274. package/dist/editor/utils/sanitizeRichtextDefaults.js +0 -26
  275. package/dist/editor/utils/sanitizeRichtextDefaults.js.map +1 -1
  276. package/dist/editor/utils/snapshotToPuckData.js +0 -8
  277. package/dist/editor/utils/snapshotToPuckData.js.map +1 -1
  278. package/dist/editor/utils/syncComponentRegistry.d.ts +2 -2
  279. package/dist/editor/utils/syncComponentRegistry.js +1 -76
  280. package/dist/editor/utils/syncComponentRegistry.js.map +1 -1
  281. package/dist/editor/utils/syncComponentRegistryWriteOnly.d.ts +7 -7
  282. package/dist/editor/utils/syncComponentRegistryWriteOnly.js +0 -22
  283. package/dist/editor/utils/syncComponentRegistryWriteOnly.js.map +1 -1
  284. package/dist/editor/utils/templatePath.js +0 -5
  285. package/dist/editor/utils/templatePath.js.map +1 -1
  286. package/dist/features/content-type-templates/editor/useTemplatePermissions.js +0 -3
  287. package/dist/features/content-type-templates/editor/useTemplatePermissions.js.map +1 -1
  288. package/dist/features/content-type-templates/hooks/useTemplateList.js +0 -30
  289. package/dist/features/content-type-templates/hooks/useTemplateList.js.map +1 -1
  290. package/dist/features/content-type-templates/index.js +0 -8
  291. package/dist/features/content-type-templates/index.js.map +1 -1
  292. package/dist/features/content-type-templates/permissions/createPuckPermissions.js +1 -43
  293. package/dist/features/content-type-templates/permissions/createPuckPermissions.js.map +1 -1
  294. package/dist/features/content-type-templates/permissions/role-permissions.js +0 -29
  295. package/dist/features/content-type-templates/permissions/role-permissions.js.map +1 -1
  296. package/dist/features/content-type-templates/permissions/useContentRole.js +0 -24
  297. package/dist/features/content-type-templates/permissions/useContentRole.js.map +1 -1
  298. package/dist/features/content-type-templates/permissions/useResolveContentRole.d.ts +5 -5
  299. package/dist/features/content-type-templates/permissions/useResolveContentRole.js +2 -22
  300. package/dist/features/content-type-templates/permissions/useResolveContentRole.js.map +1 -1
  301. package/dist/features/content-type-templates/stores/index.js +0 -5
  302. package/dist/features/content-type-templates/stores/index.js.map +1 -1
  303. package/dist/features/content-type-templates/stores/template-store-api.js +0 -14
  304. package/dist/features/content-type-templates/stores/template-store-api.js.map +1 -1
  305. package/dist/features/content-type-templates/stores/template-store.d.ts +1 -1
  306. package/dist/features/content-type-templates/stores/template-store.js +0 -12
  307. package/dist/features/content-type-templates/stores/template-store.js.map +1 -1
  308. package/dist/features/content-type-templates/types.js +0 -5
  309. package/dist/features/content-type-templates/types.js.map +1 -1
  310. package/dist/features/content-type-templates/ui/ActionBarPinButton.js +12 -21
  311. package/dist/features/content-type-templates/ui/ActionBarPinButton.js.map +1 -1
  312. package/dist/features/content-type-templates/ui/TemplateDetailsPanel.js +0 -23
  313. package/dist/features/content-type-templates/ui/TemplateDetailsPanel.js.map +1 -1
  314. package/dist/features/content-type-templates/ui/TemplateSelector.js +0 -19
  315. package/dist/features/content-type-templates/ui/TemplateSelector.js.map +1 -1
  316. package/dist/features/content-type-templates/validation/structural-validation.js +0 -40
  317. package/dist/features/content-type-templates/validation/structural-validation.js.map +1 -1
  318. package/dist/index.d.ts +6 -4
  319. package/dist/index.d.ts.map +1 -1
  320. package/dist/index.js +2 -43
  321. package/dist/index.js.map +1 -1
  322. package/dist/merge/components/conflict-notifications/ConflictNotificationToast.js +0 -19
  323. package/dist/merge/components/conflict-notifications/ConflictNotificationToast.js.map +1 -1
  324. package/dist/merge/components/conflict-notifications/index.js +0 -5
  325. package/dist/merge/components/conflict-notifications/index.js.map +1 -1
  326. package/dist/merge/components/conflict-notifications/useConflictNotifications.js +0 -48
  327. package/dist/merge/components/conflict-notifications/useConflictNotifications.js.map +1 -1
  328. package/dist/merge/components/conflict-resolution/ComponentConflictGroup.js +0 -36
  329. package/dist/merge/components/conflict-resolution/ComponentConflictGroup.js.map +1 -1
  330. package/dist/merge/components/conflict-resolution/PuckFieldResolutionPanel.js +0 -40
  331. package/dist/merge/components/conflict-resolution/PuckFieldResolutionPanel.js.map +1 -1
  332. package/dist/merge/components/conflict-resolution/RenderedResolutionPreview.js +0 -21
  333. package/dist/merge/components/conflict-resolution/RenderedResolutionPreview.js.map +1 -1
  334. package/dist/merge/components/conflict-resolution/index.js +0 -5
  335. package/dist/merge/components/conflict-resolution/index.js.map +1 -1
  336. package/dist/merge/components/merge-resolution/CherryPickVisualPanel.js +0 -44
  337. package/dist/merge/components/merge-resolution/CherryPickVisualPanel.js.map +1 -1
  338. package/dist/merge/components/merge-resolution/ComponentClickOverlay.js +0 -24
  339. package/dist/merge/components/merge-resolution/ComponentClickOverlay.js.map +1 -1
  340. package/dist/merge/components/merge-resolution/DocumentResolutionDetail.d.ts.map +1 -1
  341. package/dist/merge/components/merge-resolution/DocumentResolutionDetail.js +6 -31
  342. package/dist/merge/components/merge-resolution/DocumentResolutionDetail.js.map +1 -1
  343. package/dist/merge/components/merge-resolution/index.js +0 -3
  344. package/dist/merge/components/merge-resolution/index.js.map +1 -1
  345. package/dist/merge/useMergeResolution.d.ts +1 -1
  346. package/dist/merge/useMergeResolution.d.ts.map +1 -1
  347. package/dist/merge/useMergeResolution.js +6 -75
  348. package/dist/merge/useMergeResolution.js.map +1 -1
  349. package/dist/merge/utils/namespacePuckData.d.ts.map +1 -1
  350. package/dist/merge/utils/namespacePuckData.js +8 -8
  351. package/dist/merge/utils/namespacePuckData.js.map +1 -1
  352. package/dist/merge/utils/puckFieldClassifier.js +0 -183
  353. package/dist/merge/utils/puckFieldClassifier.js.map +1 -1
  354. package/dist/p1/editor/connect/connect-preview-config.js +0 -5
  355. package/dist/p1/editor/connect/connect-preview-config.js.map +1 -1
  356. package/dist/p1/editor/connect/field-connect-plugin.js +0 -6
  357. package/dist/p1/editor/connect/field-connect-plugin.js.map +1 -1
  358. package/dist/p1/editor/editor-preview-resolve.js +0 -13
  359. package/dist/p1/editor/editor-preview-resolve.js.map +1 -1
  360. package/dist/p1/editor/hooks/api-hooks.js +0 -7
  361. package/dist/p1/editor/hooks/api-hooks.js.map +1 -1
  362. package/dist/p1/editor/hooks/useLiveEditorContext.js +0 -12
  363. package/dist/p1/editor/hooks/useLiveEditorContext.js.map +1 -1
  364. package/dist/p1/editor/hooks/useLiveRemoteDatasources.js +0 -22
  365. package/dist/p1/editor/hooks/useLiveRemoteDatasources.js.map +1 -1
  366. package/dist/p1/editor/hooks/useP1Plugins.js +0 -5
  367. package/dist/p1/editor/hooks/useP1Plugins.js.map +1 -1
  368. package/dist/p1/editor/remote-datasources/remote-datasource-explorer-plugin.js +0 -7
  369. package/dist/p1/editor/remote-datasources/remote-datasource-explorer-plugin.js.map +1 -1
  370. package/dist/p1/editor/template-autocomplete-layer.js +0 -4
  371. package/dist/p1/editor/template-autocomplete-layer.js.map +1 -1
  372. package/dist/pds/components/AgentChip.js +0 -3
  373. package/dist/pds/components/AgentChip.js.map +1 -1
  374. package/dist/pds/components/CreatePageModal.js +1 -97
  375. package/dist/pds/components/CreatePageModal.js.map +1 -1
  376. package/dist/pds/components/LoadingMessage.d.ts.map +1 -1
  377. package/dist/pds/components/LoadingMessage.js +2 -1
  378. package/dist/pds/components/LoadingMessage.js.map +1 -1
  379. package/dist/pds/components/LoadingMessage.module.css +2 -21
  380. package/dist/pds/components/LoadingOverlay.d.ts +17 -0
  381. package/dist/pds/components/LoadingOverlay.d.ts.map +1 -0
  382. package/dist/pds/components/LoadingOverlay.js +7 -0
  383. package/dist/pds/components/LoadingOverlay.js.map +1 -0
  384. package/dist/pds/components/LoadingOverlay.module.css +28 -0
  385. package/dist/pds/components/P1EditorHeader.d.ts +9 -2
  386. package/dist/pds/components/P1EditorHeader.d.ts.map +1 -1
  387. package/dist/pds/components/P1EditorHeader.js +6 -26
  388. package/dist/pds/components/P1EditorHeader.js.map +1 -1
  389. package/dist/pds/components/P1EditorHeader.module.css +26 -0
  390. package/dist/pds/components/PageNavigator.js +0 -16
  391. package/dist/pds/components/PageNavigator.js.map +1 -1
  392. package/dist/pds/components/PresenceStack.js +0 -9
  393. package/dist/pds/components/PresenceStack.js.map +1 -1
  394. package/dist/pds/components/PublishControl.js +0 -6
  395. package/dist/pds/components/PublishControl.js.map +1 -1
  396. package/dist/pds/components/WorkstreamSwitcher.js +1 -25
  397. package/dist/pds/components/WorkstreamSwitcher.js.map +1 -1
  398. package/dist/pds/index.d.ts +2 -0
  399. package/dist/pds/index.d.ts.map +1 -1
  400. package/dist/pds/index.js +1 -29
  401. package/dist/pds/index.js.map +1 -1
  402. package/dist/pds/theme/pds-core-content.js +0 -5
  403. package/dist/pds/theme/pds-core-content.js.map +1 -1
  404. package/dist/pds/utils/deriveDocState.js +0 -1
  405. package/dist/pds/utils/deriveDocState.js.map +1 -1
  406. package/dist/pds/utils/deriveLiveDocState.js +0 -13
  407. package/dist/pds/utils/deriveLiveDocState.js.map +1 -1
  408. package/dist/registry-sync.js +0 -4
  409. package/dist/registry-sync.js.map +1 -1
  410. package/dist/server.js +0 -7
  411. package/dist/server.js.map +1 -1
  412. package/dist/versioning/components/ReadOnlyFieldsGuard.js +0 -3
  413. package/dist/versioning/components/ReadOnlyFieldsGuard.js.map +1 -1
  414. package/dist/versioning/components/VersionBannerActions.js +0 -1
  415. package/dist/versioning/components/VersionBannerActions.js.map +1 -1
  416. package/dist/versioning/components/version-compare/BranchDiffHeader.js +0 -7
  417. package/dist/versioning/components/version-compare/BranchDiffHeader.js.map +1 -1
  418. package/dist/versioning/components/version-compare/BranchMergeCompare.js +0 -19
  419. package/dist/versioning/components/version-compare/BranchMergeCompare.js.map +1 -1
  420. package/dist/versioning/components/version-compare/ComponentNode.js +0 -8
  421. package/dist/versioning/components/version-compare/ComponentNode.js.map +1 -1
  422. package/dist/versioning/components/version-compare/ComponentTree.js +0 -10
  423. package/dist/versioning/components/version-compare/ComponentTree.js.map +1 -1
  424. package/dist/versioning/components/version-compare/DiffHeader.js +0 -3
  425. package/dist/versioning/components/version-compare/DiffHeader.js.map +1 -1
  426. package/dist/versioning/components/version-compare/DocumentDiffList.js +0 -14
  427. package/dist/versioning/components/version-compare/DocumentDiffList.js.map +1 -1
  428. package/dist/versioning/components/version-compare/PropDiffPanel.js +0 -7
  429. package/dist/versioning/components/version-compare/PropDiffPanel.js.map +1 -1
  430. package/dist/versioning/components/version-compare/PropDiffRow.js +0 -3
  431. package/dist/versioning/components/version-compare/PropDiffRow.js.map +1 -1
  432. package/dist/versioning/components/version-compare/PropValueDisplay.js +0 -22
  433. package/dist/versioning/components/version-compare/PropValueDisplay.js.map +1 -1
  434. package/dist/versioning/components/version-compare/VersionComparePage.js +0 -13
  435. package/dist/versioning/components/version-compare/VersionComparePage.js.map +1 -1
  436. package/dist/versioning/components/version-compare/VisualBranchCompare.js +0 -17
  437. package/dist/versioning/components/version-compare/VisualBranchCompare.js.map +1 -1
  438. package/dist/versioning/components/version-compare/VisualVersionCompare.js +0 -23
  439. package/dist/versioning/components/version-compare/VisualVersionCompare.js.map +1 -1
  440. package/dist/versioning/components/version-compare/index.js +0 -5
  441. package/dist/versioning/components/version-compare/index.js.map +1 -1
  442. package/dist/versioning/components/version-history/AgentCheckpointBadge.js +0 -14
  443. package/dist/versioning/components/version-history/AgentCheckpointBadge.js.map +1 -1
  444. package/dist/versioning/components/version-history/VersionItem.js +0 -7
  445. package/dist/versioning/components/version-history/VersionItem.js.map +1 -1
  446. package/dist/versioning/components/version-history/index.js +0 -5
  447. package/dist/versioning/components/version-history/index.js.map +1 -1
  448. package/dist/versioning/useCheckpoints.js +0 -28
  449. package/dist/versioning/useCheckpoints.js.map +1 -1
  450. package/dist/versioning/useVersions.js +0 -31
  451. package/dist/versioning/useVersions.js.map +1 -1
  452. package/dist/versioning/utils/branchDiff.js +0 -93
  453. package/dist/versioning/utils/branchDiff.js.map +1 -1
  454. package/dist/versioning/utils/diff.js +0 -79
  455. package/dist/versioning/utils/diff.js.map +1 -1
  456. package/dist/versioning/utils/formatVersionDate.js +0 -12
  457. package/dist/versioning/utils/formatVersionDate.js.map +1 -1
  458. package/dist/versioning/utils/highlightConfig.js +0 -42
  459. package/dist/versioning/utils/highlightConfig.js.map +1 -1
  460. package/dist/versioning/utils/versionKind.js +0 -10
  461. package/dist/versioning/utils/versionKind.js.map +1 -1
  462. package/package.json +7 -7
  463. package/src/pds/theme/PuckEditorTheme.css +5 -5
  464. package/src/styles.css +1 -1
  465. package/src/pds/theme/PuckEditorTheme.test.ts +0 -195
  466. package/src/pds/theme/pds-core-content.ts +0 -8
@@ -1,9 +1,4 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- /**
3
- * P1 Puck Provider
4
- *
5
- * React context provider for P1 Puck integration.
6
- */
7
2
  import { useState, useCallback, useMemo, useEffect, useRef } from 'react';
8
3
  import isEqual from 'lodash.isequal';
9
4
  import { P1PuckContext } from '../core/P1PuckContext.js';
@@ -16,75 +11,21 @@ import { resolveFeatureConfig } from '../core/featureConfig.js';
16
11
  import { createPuckPermissions } from '../features/content-type-templates/permissions/createPuckPermissions.js';
17
12
  import { useTemplateList } from '../features/content-type-templates/hooks/useTemplateList.js';
18
13
  import { presenceIdentityKey } from '../collaboration/utils/presenceIdentity.js';
19
- import { DEFAULT_CSS_FEATURE_PLUGINS } from './defaultPlugins.js';
14
+ import { DEFAULT_CCR_FEATURE_PLUGINS } from './defaultPlugins.js';
20
15
  import { resolveActivePlugins, composeProviders } from './composePlugins.js';
21
16
  import { snapshotToPuckData } from './utils/snapshotToPuckData.js';
22
17
  import { useDocuments } from './useDocuments.js';
23
18
  import { useRealtime } from './useRealtime.js';
24
- /**
25
- * Identity to record alongside a loaded payload.
26
- *
27
- * `branch` must be the branch the load was requested for, not the branch on the
28
- * returned version: for an inherited document the backend serves main's
29
- * published version, and recording main would make every consumer treat the
30
- * payload as belonging to another workstream.
31
- *
32
- * Returns a fresh object per call so consumers re-run even when two documents
33
- * share payload identity — snapshotToPuckData yields one shared constant for
34
- * every blank snapshot.
35
- */
36
19
  function originFor(doc, branch, versionId, historical) {
37
20
  return doc
38
21
  ? { branchId: branch, documentId: doc.id, documentPath: doc.path, versionId, historical }
39
22
  : null;
40
23
  }
41
- /**
42
- * Provider component for P1 Puck integration.
43
- *
44
- * Wraps your Puck editor to provide P1 functionality including:
45
- * - Auto-save with debouncing
46
- * - Document loading
47
- * - Checkpoint (publish) creation
48
- * - Branch switching
49
- * - Toast notifications for errors and success
50
- *
51
- * @example
52
- * ```tsx
53
- * import { P1PuckProvider, P1PuckEditor } from '@pantheon-systems/puck-css';
54
- * import { P1Client } from '@pantheon-systems/css-client';
55
- *
56
- * const client = new P1Client({
57
- * baseUrl: 'http://localhost:8787',
58
- * apiKey: 'your-api-key',
59
- * });
60
- *
61
- * function App() {
62
- * return (
63
- * <P1PuckProvider
64
- * client={client}
65
- * siteId="site-123"
66
- * branchId="branch-456"
67
- * userId="user-789"
68
- * >
69
- * <P1PuckEditor config={puckConfig} documentPath="/home" />
70
- * </P1PuckProvider>
71
- * );
72
- * }
73
- * ```
74
- */
75
24
  export function P1PuckProvider(props) {
76
25
  return (_jsx(NotificationProvider, { children: _jsx(P1PuckProviderInner, { ...props }) }));
77
26
  }
78
- /** Query param naming the workstream to open, set by the dashboard's "Open in visual editor". */
79
27
  const BRANCH_QUERY_PARAM = 'branch';
80
- /**
81
- * Rejected here rather than downstream: the value reaches API paths and the
82
- * realtime URL unescaped, so anything that could change a URL's shape ("/",
83
- * ".", "?", "#") must never get that far. refreshBranches still decides whether
84
- * a well-formed id actually exists.
85
- */
86
28
  const BRANCH_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
87
- /** Kept pure — the param is consumed in an effect, not while rendering. */
88
29
  function readDeepLinkBranchId() {
89
30
  if (typeof window === 'undefined')
90
31
  return '';
@@ -97,10 +38,6 @@ function readDeepLinkBranchId() {
97
38
  return '';
98
39
  }
99
40
  }
100
- /**
101
- * Left in place the param would re-apply on the next remount (this provider
102
- * remounts on route-param changes), silently undoing a manual workstream switch.
103
- */
104
41
  function stripDeepLinkBranchParam() {
105
42
  if (typeof window === 'undefined')
106
43
  return;
@@ -112,37 +49,30 @@ function stripDeepLinkBranchParam() {
112
49
  window.history.replaceState(window.history.state, '', `${url.pathname}${url.search}${url.hash}`);
113
50
  }
114
51
  catch {
115
- // history/URL unavailable — the param is harmless if it stays
116
52
  }
117
53
  }
118
- /**
119
- * Inner provider component that has access to notification context.
120
- */
121
- function P1PuckProviderInner({ client, siteId, branchId: initialBranchId, userId, autoSaveDelay = 3000, maxRetries = 3, showErrorNotifications = true, enableRealtime = true, wsBaseUrl, realtimeApiKey, realtimeSyncInterval: _realtimeSyncInterval = 250, realtimeTokenRefresher,
122
- // Phase 9: Presence props
123
- presenceEnabled = true, presencePollingInterval = 5000, userName: _userName, userAvatar: _userAvatar, userNameResolver,
124
- // Phase 9: Agent mode props
125
- agentModeEnabled = false, agentId, agentTrigger: _agentTrigger,
126
- // Phase 9: Callbacks
127
- onPresenceChange, onAgentConflict: _onAgentConflict,
128
- // Plugin system props (B.4)
129
- featurePlugins, featureConfig,
130
- // Content Type Templates (PROPOSAL-010)
131
- // Consumers should resolve the role via useResolveContentRole and pass it here.
132
- userRole = 'editor', children, }) {
133
- // Access notification context
54
+ function P1PuckProviderInner({ client, siteId, branchId: initialBranchId, userId, autoSaveDelay = 3000, maxRetries = 3, showErrorNotifications = true, enableRealtime = true, wsBaseUrl, realtimeApiKey, realtimeSyncInterval: _realtimeSyncInterval = 250, realtimeTokenRefresher, presenceEnabled = true, presencePollingInterval = 5000, userName: _userName, userAvatar: _userAvatar, userNameResolver, agentModeEnabled = false, agentId, agentTrigger: _agentTrigger, onPresenceChange, onAgentConflict: _onAgentConflict, featurePlugins, featureConfig, userRole = 'editor', children, }) {
134
55
  const notificationContext = useNotifications();
135
- // Persist selected branch in sessionStorage so it survives provider remounts
136
- // (e.g. when P1App is rendered per-page instead of in a shared layout).
137
- const branchStorageKey = `css-branch-${siteId}`;
56
+ const branchStorageKey = `ccr-branch-${siteId}`;
57
+ const legacyBranchStorageKey = `css-branch-${siteId}`;
138
58
  const getPersistedBranchId = useCallback(() => {
139
59
  try {
140
- return (typeof window !== 'undefined' && sessionStorage.getItem(branchStorageKey)) || '';
60
+ if (typeof window === 'undefined')
61
+ return '';
62
+ const persisted = sessionStorage.getItem(branchStorageKey);
63
+ if (persisted !== null)
64
+ return persisted;
65
+ const legacy = sessionStorage.getItem(legacyBranchStorageKey);
66
+ if (legacy !== null) {
67
+ sessionStorage.setItem(branchStorageKey, legacy);
68
+ sessionStorage.removeItem(legacyBranchStorageKey);
69
+ }
70
+ return legacy || '';
141
71
  }
142
72
  catch {
143
73
  return '';
144
74
  }
145
- }, [branchStorageKey]);
75
+ }, [branchStorageKey, legacyBranchStorageKey]);
146
76
  const persistBranchId = useCallback((id) => {
147
77
  try {
148
78
  if (typeof window !== 'undefined') {
@@ -150,33 +80,21 @@ userRole = 'editor', children, }) {
150
80
  }
151
81
  }
152
82
  catch {
153
- // sessionStorage may be unavailable (SSR, privacy mode)
154
83
  }
155
84
  }, [branchStorageKey]);
156
- // Site name — fetched once on mount from the P1 API
157
85
  const [siteName, setSiteName] = useState(null);
158
86
  useEffect(() => {
159
87
  client.sites?.get(siteId)
160
88
  ?.then((site) => setSiteName(site.name))
161
89
  .catch(() => { });
162
90
  }, [client, siteId]);
163
- // Outranks the prop (a build-time default) and sessionStorage (a previous
164
- // visit): this names the workstream the user just asked for. refreshBranches
165
- // validates it, so a stale or foreign id still falls back to main.
166
91
  const [deepLinkBranchId] = useState(readDeepLinkBranchId);
167
- // Branch state - start with the deep link, initialBranchId, persisted branch,
168
- // or empty (will be set to main)
169
92
  const [branchId, setBranchId] = useState(() => {
170
93
  if (deepLinkBranchId)
171
94
  return deepLinkBranchId;
172
95
  if (initialBranchId)
173
96
  return initialBranchId;
174
- try {
175
- return (typeof window !== 'undefined' && sessionStorage.getItem(branchStorageKey)) || '';
176
- }
177
- catch {
178
- return '';
179
- }
97
+ return getPersistedBranchId();
180
98
  });
181
99
  const [branches, setBranches] = useState([]);
182
100
  const [branchResolutionError, setBranchResolutionError] = useState(null);
@@ -184,115 +102,48 @@ userRole = 'editor', children, }) {
184
102
  const [branchesLoading, setBranchesLoading] = useState(() => {
185
103
  if (deepLinkBranchId || initialBranchId)
186
104
  return false;
187
- try {
188
- return !(typeof window !== 'undefined' && sessionStorage.getItem(branchStorageKey));
189
- }
190
- catch {
191
- return true;
192
- }
105
+ return !getPersistedBranchId();
193
106
  });
194
- // Persist before stripping: once the param is gone, a remount landing before
195
- // the branch list resolves would otherwise fall back to a stale persisted
196
- // branch. refreshBranches replaces this with a validated value.
197
107
  useEffect(() => {
198
108
  if (!deepLinkBranchId)
199
109
  return;
200
110
  persistBranchId(deepLinkBranchId);
201
111
  stripDeepLinkBranchParam();
202
112
  }, [deepLinkBranchId, persistBranchId]);
203
- // Document state
204
113
  const [currentDocument, setCurrentDocument] = useState(null);
205
114
  const [currentData, setCurrentData] = useState(null);
206
115
  const [currentDataOrigin, setCurrentDataOrigin] = useState(null);
207
- // True while loadDocument is in flight. Consumers use it to distinguish
208
- // "no document while switching" (keep showing the old canvas) from
209
- // "genuinely no document" (show the empty state).
210
116
  const [documentLoading, setDocumentLoading] = useState(false);
211
- // Template state (PROPOSAL-010)
212
117
  const [currentTemplate, setCurrentTemplate] = useState(null);
213
- // Save state
214
118
  const [saveStatus, setSaveStatus] = useState('idle');
215
119
  const [lastSaved, setLastSaved] = useState(null);
216
120
  const [saveError, setSaveError] = useState(null);
217
- // Pending data for debounced save - use ref to avoid recreating debounce
218
121
  const pendingDataRef = useRef(null);
219
122
  const currentDocumentRef = useRef(null);
220
123
  const initializedRef = useRef(false);
221
- // Track the latest Puck action metadata for inclusion in sync payloads.
222
- // Buffered actions captured by handleAction (Puck's onAction callback)
223
- // Array accumulates all structural actions during an edit session.
224
- // Forwarded to backend on save for version history, then cleared.
225
124
  const pendingActionsRef = useRef([]);
226
- // Tracks the document path that the current data in state belongs to.
227
- // Set alongside every setCurrentData call to record the data's origin.
228
- // Checked before every write (realtime or REST) to prevent cross-document
229
- // corruption during rapid document switching.
230
125
  const currentDataDocumentPathRef = useRef(null);
231
126
  const currentDataOriginRef = useRef(null);
232
- // Commit data and the identity it was loaded under in a single render pass.
233
- //
234
- // Path alone cannot distinguish two branches' copies of the same page, which
235
- // is what let one workstream's data be published under another's sync key.
236
- // Because both land in one commit, a consumer reading them together can never
237
- // see a new identity beside old data.
238
127
  const commitCurrentData = useCallback((data, origin) => {
239
128
  currentDataDocumentPathRef.current = origin?.documentPath ?? null;
240
129
  currentDataOriginRef.current = origin;
241
130
  setCurrentDataOrigin(origin);
242
131
  setCurrentData(data);
243
132
  }, []);
244
- // Snapshot of the data loadDocument just loaded. The first onChange after a
245
- // load echoes this back; saveData drops that echo by structural compare
246
- // (not a one-shot flag, which would swallow a lone genuine first edit like a
247
- // pin toggle). Structural, not string: Puck re-normalizes key order.
248
133
  const suppressNextSaveRef = useRef(null);
249
- // Monotonically increasing counter for stale loadDocument response detection.
250
- // Each loadDocument call increments this and captures the current value.
251
- // After each async operation, the captured value is compared to the current
252
- // value — if they differ, a newer loadDocument call has started and the
253
- // current one should bail out.
254
134
  const loadRequestIdRef = useRef(0);
255
- // Track realtime connection state for use in performSave (avoids stale closure)
256
135
  const realtimeConnectedRef = useRef(false);
257
- // Auto-save pause state
258
136
  const [autoSavePaused, setAutoSavePaused] = useState(false);
259
- // Version viewing state
260
137
  const [viewingVersion, setViewingVersion] = useState(null);
261
- // True while returnToLatest is fetching the latest version from the server,
262
- // so the banner can show progress during the round trip.
263
138
  const [isReturningToLatest, setIsReturningToLatest] = useState(false);
264
- // Last known "latest" snapshot — a write-only internal fallback for
265
- // returnToLatest when no live Yjs snapshot is available and the server
266
- // re-fetch fails. Backed by a ref (not state): nothing renders from it, so
267
- // making it state would add a setState on every autosave and churn the
268
- // context memo / returnToLatest identity for no consumer benefit.
269
139
  const latestVersionDataRef = useRef(null);
270
- // Remote sync key - changes when remote updates arrive to trigger Puck sync
271
140
  const [remoteSyncKey, setRemoteSyncKey] = useState(null);
272
- // Counter to track pending remote updates that will trigger onChange callbacks.
273
- // When we receive a remote update and sync it to Puck via setData, Puck fires onChange.
274
- // We need to skip the realtime send for that onChange to prevent bounce-back loops.
275
- // Using a counter is more reliable than a timeout because it's synchronized with
276
- // the actual number of remote updates received.
277
141
  const pendingRemoteUpdatesRef = useRef(0);
278
- // Flag to track if we're currently applying a remote sync.
279
- // This is set before setCurrentData and cleared after a short delay to ensure
280
- // all related onChange events are skipped (there may be multiple: one from data
281
- // prop change and one from setData dispatch).
282
- // Pending remote data ref - stores latest data during debounce period
283
142
  const pendingRemoteDataRef = useRef(null);
284
- // Debounce timer for remote sync - batches rapid updates to reduce flickering
285
143
  const remoteSyncTimerRef = useRef(null);
286
- // Delay for debouncing remote sync updates (ms)
287
144
  const REMOTE_SYNC_DEBOUNCE_DELAY = 50;
288
- // Ref to track viewingVersion for use in callbacks (avoids stale closure)
289
145
  const viewingVersionRef = useRef(null);
290
- // Tracks the latest data seen by saveData when NOT viewing historical.
291
- // Used by returnToLatest to restore unsaved edits that were in Puck's
292
- // memory when the user navigated to a historical version.
293
146
  const latestLocalDataRef = useRef(null);
294
- // Helper to cancel any pending remote sync - used when loading documents/versions
295
- // This prevents race conditions where a pending remote update overrides loaded data
296
147
  const cancelPendingRemoteSync = useCallback(() => {
297
148
  if (remoteSyncTimerRef.current) {
298
149
  clearTimeout(remoteSyncTimerRef.current);
@@ -300,10 +151,8 @@ userRole = 'editor', children, }) {
300
151
  }
301
152
  pendingRemoteDataRef.current = null;
302
153
  }, []);
303
- // WebSocket presence state - used when realtime is connected for instant presence updates
304
154
  const [wsPresenceActors, setWsPresenceActors] = useState([]);
305
155
  const [wsPresenceActive, setWsPresenceActive] = useState(false);
306
- // Helper to enrich actors with display names from the resolver
307
156
  const enrichActorsWithNames = useCallback((actors) => {
308
157
  if (!userNameResolver)
309
158
  return actors;
@@ -312,7 +161,8 @@ userRole = 'editor', children, }) {
312
161
  return resolvedName ? { ...actor, name: resolvedName } : actor;
313
162
  });
314
163
  }, [userNameResolver]);
315
- // Real-time collaboration hook
164
+ const [baselineResetKey, setBaselineResetKey] = useState(0);
165
+ const loadDocForResetRef = useRef(null);
316
166
  const realtime = useRealtime({
317
167
  baseUrl: wsBaseUrl ?? '',
318
168
  apiKey: realtimeApiKey,
@@ -324,47 +174,48 @@ userRole = 'editor', children, }) {
324
174
  actorType: 'user',
325
175
  enabled: enableRealtime && !!wsBaseUrl,
326
176
  initialData: currentData,
177
+ resetKey: baselineResetKey,
327
178
  onServerReload: () => {
328
179
  notificationContext.addInfo('Document is being refreshed by the server. Reconnecting...');
329
180
  },
330
- // WebSocket presence callbacks - receive instant presence updates
181
+ onBaselineReset: () => {
182
+ notificationContext.addInfo('Your local changes could not be synced after a branch merge. Reconnecting with fresh content...');
183
+ const docPath = currentDocumentRef.current?.path;
184
+ if (!docPath)
185
+ return;
186
+ void loadDocForResetRef.current?.(docPath)
187
+ .then(() => {
188
+ setBaselineResetKey((k) => k + 1);
189
+ })
190
+ .catch(() => {
191
+ setBaselineResetKey((k) => k + 1);
192
+ notificationContext.addError('Could not reload content after a branch merge. Reconnecting — you may need to refresh if the editor stays blank.');
193
+ });
194
+ },
331
195
  onPresenceUpdate: (actors) => {
332
- // Mark WebSocket presence as active (first update received)
333
196
  setWsPresenceActive(true);
334
- // Filter out self, enrich with names, and update state
335
197
  const filtered = actors.filter((a) => a.actorId !== userId);
336
198
  const enriched = enrichActorsWithNames(filtered);
337
199
  setWsPresenceActors(enriched);
338
200
  },
339
201
  onFocusRegionBroadcast: (actorId, focusRegions) => {
340
- // Update focus regions for the specific actor
341
202
  setWsPresenceActors((prev) => prev.map((a) => (a.actorId === actorId ? { ...a, focusRegions } : a)));
342
203
  },
343
204
  onRemoteUpdate: (data) => {
344
205
  if (viewingVersionRef.current !== null) {
345
206
  return;
346
207
  }
347
- // Reject empty data that would overwrite real editor content.
348
- // During initial Yjs sync, the remote doc may be empty, producing
349
- // { content: [], root: { props: {} } }. Applying this would clear
350
- // the editor and trigger a save loop.
351
208
  const rootProps = data.root.props;
352
209
  if (data.content.length === 0 &&
353
210
  (!rootProps || Object.keys(rootProps).length === 0) &&
354
211
  !data.zones) {
355
212
  return;
356
213
  }
357
- // Store the latest data - will be used when debounce fires
358
214
  pendingRemoteDataRef.current = data;
359
- // Clear any pending debounce timer
360
215
  if (remoteSyncTimerRef.current) {
361
216
  clearTimeout(remoteSyncTimerRef.current);
362
217
  }
363
- // Debounce the sync to batch rapid successive updates
364
- // This prevents plugin recreation and flickering on every update
365
218
  remoteSyncTimerRef.current = setTimeout(() => {
366
- // Double-check we're still not viewing a historical version
367
- // (user might have switched while debounce was pending)
368
219
  if (viewingVersionRef.current !== null) {
369
220
  pendingRemoteDataRef.current = null;
370
221
  remoteSyncTimerRef.current = null;
@@ -372,22 +223,10 @@ userRole = 'editor', children, }) {
372
223
  }
373
224
  const dataToSync = pendingRemoteDataRef.current;
374
225
  if (dataToSync) {
375
- // Increment counter to skip the onChange echo(es) that will fire
376
- // when Puck processes the setCurrentData call below.
377
- // The content-based guards in puckDataToYMap and RealtimeClient
378
- // prevent actual echo loops at the Y.Doc/WebSocket layer.
379
226
  pendingRemoteUpdatesRef.current += 1;
380
- // Update current data when remote changes arrive. Same document and
381
- // branch as whatever is loaded, but no longer a stored version — this
382
- // syncs through remoteSyncKey below, not the document-sync plugin.
383
227
  commitCurrentData(dataToSync, originFor(currentDocumentRef.current, currentDataOriginRef.current?.branchId ?? null, null, currentDataOriginRef.current?.historical ?? false));
384
- // Update sync key to trigger Puck re-sync
385
228
  setRemoteSyncKey(`remote-${Date.now()}`);
386
229
  pendingRemoteDataRef.current = null;
387
- // Safety net: reset counter after React has processed the state updates.
388
- // If the remote data is identical to current editor state, Puck won't
389
- // fire onChange and the counter would never be decremented naturally.
390
- // Leaving it > 0 would cause subsequent local edits to be dropped.
391
230
  setTimeout(() => {
392
231
  pendingRemoteUpdatesRef.current = 0;
393
232
  }, 100);
@@ -396,10 +235,8 @@ userRole = 'editor', children, }) {
396
235
  }, REMOTE_SYNC_DEBOUNCE_DELAY);
397
236
  },
398
237
  });
399
- // Keep a stable ref to realtime for use in callbacks without dependency churn
400
238
  const realtimeRef = useRef(realtime);
401
239
  realtimeRef.current = realtime;
402
- // Cleanup remote sync timer on unmount
403
240
  useEffect(() => {
404
241
  return () => {
405
242
  if (remoteSyncTimerRef.current) {
@@ -407,33 +244,16 @@ userRole = 'editor', children, }) {
407
244
  }
408
245
  };
409
246
  }, []);
410
- // PuckDataCapture catch-up: detects data that Puck's onChange missed.
411
- // Puck's onChange can miss the very last keystroke in a typing burst because
412
- // createOnChange dispatches asynchronously (via resolveComponentData) and
413
- // the Zustand subscriber may not fire for the final state update in time.
414
- // PuckDataCapture subscribes to Puck's Zustand store independently via
415
- // createUsePuck() and writes the true current data to this ref.
416
- // The onDataChange callback debounces and compares with what saveData last
417
- // sent — if they differ, it pushes the corrected data through realtime.
418
247
  const realtimeDataCaptureRef = useRef(null);
419
248
  const lastSentDataRef = useRef(null);
420
249
  const captureTimerRef = useRef(null);
421
- // Track the last data sent via saveData so we can detect missed updates.
422
- // Updated in saveData's realtime branch (the happy path).
423
250
  const trackSentData = useCallback((data) => {
424
251
  lastSentDataRef.current = JSON.stringify(data);
425
252
  }, []);
426
- // Callback for PuckDataCapture.onDataChange — fires when Puck's store
427
- // updates (may fire for changes that onChange misses).
428
253
  const handleRealtimeDataCapture = useCallback((data) => {
429
254
  if (!enableRealtime)
430
255
  return;
431
- // Store the latest data in the ref for comparison when the timer fires.
432
- // The ref always holds the most recent Zustand store snapshot.
433
256
  realtimeDataCaptureRef.current = data;
434
- // Debounce: wait 800ms after the last store update to allow onChange
435
- // to process normally. When the timer fires, compare the ref's current
436
- // data (not the closure's stale copy) against what was last sent.
437
257
  if (captureTimerRef.current)
438
258
  clearTimeout(captureTimerRef.current);
439
259
  captureTimerRef.current = setTimeout(() => {
@@ -445,10 +265,8 @@ userRole = 'editor', children, }) {
445
265
  if (!currentData)
446
266
  return;
447
267
  const dataJson = JSON.stringify(currentData);
448
- // If saveData already sent this exact data, no catch-up needed
449
268
  if (dataJson === lastSentDataRef.current)
450
269
  return;
451
- // Data origin guard
452
270
  const currentPath = currentDocumentRef.current?.path ?? null;
453
271
  const dataOriginPath = currentDataDocumentPathRef.current;
454
272
  if (dataOriginPath !== currentPath)
@@ -457,31 +275,24 @@ userRole = 'editor', children, }) {
457
275
  lastSentDataRef.current = dataJson;
458
276
  }, 800);
459
277
  }, [enableRealtime]);
460
- // Cleanup capture timer on unmount
461
278
  useEffect(() => {
462
279
  return () => {
463
280
  if (captureTimerRef.current)
464
281
  clearTimeout(captureTimerRef.current);
465
282
  };
466
283
  }, []);
467
- // Keep refs in sync with state
468
284
  useEffect(() => {
469
285
  currentDocumentRef.current = currentDocument;
470
286
  }, [currentDocument]);
471
287
  useEffect(() => {
472
288
  viewingVersionRef.current = viewingVersion;
473
289
  }, [viewingVersion]);
474
- // Keep realtime connection ref in sync (used by performSave to avoid stale closure)
475
290
  useEffect(() => {
476
291
  realtimeConnectedRef.current = realtime.connected;
477
292
  }, [realtime.connected]);
478
- // Create client with user principal
479
293
  const userClient = useMemo(() => client.withPrincipal({ id: userId, type: 'user' }), [client, userId]);
480
- // Document list for current branch
481
294
  const { documents: branchDocuments, loading: documentsLoading, error: documentsError, create: createDocumentRaw, remove: removeDocumentRaw, refresh: refreshDocuments, } = useDocuments({ client: userClient, siteId, branchId });
482
- // Template list for current branch
483
295
  const { templates: branchTemplates, loading: templatesLoading, error: templatesError, refresh: refreshTemplates, } = useTemplateList(userClient, siteId, branchId);
484
- // Stable document create/delete callbacks
485
296
  const branchIdRef = useRef(branchId);
486
297
  branchIdRef.current = branchId;
487
298
  const createDocumentRawRef = useRef(createDocumentRaw);
@@ -491,16 +302,10 @@ userRole = 'editor', children, }) {
491
302
  throw new Error('Cannot create document: no branch selected');
492
303
  }
493
304
  if (template) {
494
- // The list endpoint carries no layout content; fetch the full template.
495
305
  const fullTemplate = await userClient.templates.get(siteId, branchIdRef.current, template.id);
496
- // A template without a content array has no stored layout snapshot;
497
- // creating a page from it would silently produce a blank page. An empty
498
- // array is a legitimate empty layout and passes.
499
306
  if (!fullTemplate.content) {
500
307
  throw new Error(`Template "${template.label || template.name}" has no layout yet. Open it in the editor and add components before creating pages from it.`);
501
308
  }
502
- // The backend builds the initial version from the template, preserving
503
- // each component's durable slot id; no client snapshot is sent.
504
309
  await createDocumentRawRef.current(path, undefined, {
505
310
  templateId: fullTemplate.id,
506
311
  templateVersion: fullTemplate.version,
@@ -511,14 +316,8 @@ userRole = 'editor', children, }) {
511
316
  await createDocumentRawRef.current(path, undefined, title ? { title } : undefined);
512
317
  }
513
318
  }, [userClient, siteId]);
514
- // Stable update-template callback (template metadata: label/description/URL
515
- // pattern). Used by the editor's template-mode right sidebar to persist details
516
- // to the Template record, then refresh the list so the changes propagate.
517
319
  const refreshTemplatesRef = useRef(refreshTemplates);
518
320
  refreshTemplatesRef.current = refreshTemplates;
519
- // Create a new template (empty layout, built in the template editor
520
- // afterwards). Returns the created Template so callers can navigate to its
521
- // editor. Used by the Create Page modal's "New template" flow.
522
321
  const stableCreateTemplate = useCallback(async (params) => {
523
322
  if (!branchIdRef.current) {
524
323
  throw new Error('Cannot create template: no branch selected');
@@ -532,7 +331,6 @@ userRole = 'editor', children, }) {
532
331
  throw new Error('Cannot update template: no branch selected');
533
332
  }
534
333
  const updated = await userClient.templates.update(siteId, branchIdRef.current, templateId, params);
535
- // Keep the in-editor template in sync if it's the one being edited.
536
334
  setCurrentTemplate((prev) => (prev && prev.id === updated.id ? updated : prev));
537
335
  await refreshTemplatesRef.current();
538
336
  }, [userClient, siteId]);
@@ -544,7 +342,6 @@ userRole = 'editor', children, }) {
544
342
  }
545
343
  await removeDocumentRawRef.current(documentId);
546
344
  }, []);
547
- // Load branches
548
345
  const refreshBranches = useCallback(async () => {
549
346
  try {
550
347
  setBranchesLoading(true);
@@ -552,33 +349,25 @@ userRole = 'editor', children, }) {
552
349
  const branchList = await userClient.branches.list(siteId);
553
350
  setBranches(branchList);
554
351
  let effectiveBranchId = branchIdRef.current;
555
- // If no branchId set, try persisted branch, then default to main
556
352
  if (!effectiveBranchId) {
557
353
  const persisted = getPersistedBranchId();
558
354
  if (persisted && branchList.some((b) => b.id === persisted)) {
559
355
  effectiveBranchId = persisted;
560
356
  }
561
357
  }
562
- // Validate that the branch exists in the list; fall back to main if not
563
358
  if (effectiveBranchId && !branchList.some((b) => b.id === effectiveBranchId)) {
564
359
  const mainBranch = branchList.find((b) => b.isMain);
565
360
  effectiveBranchId = mainBranch?.id ?? effectiveBranchId;
566
361
  }
567
- // Default to main if still empty
568
362
  if (!effectiveBranchId) {
569
363
  const mainBranch = branchList.find((b) => b.isMain);
570
364
  if (mainBranch) {
571
365
  effectiveBranchId = mainBranch.id;
572
366
  }
573
367
  }
574
- // A list that resolves without a usable branch strands the editor exactly
575
- // as a refused list does — most often a siteId that doesn't exist, which
576
- // this endpoint answers with an empty list rather than a 404.
577
368
  if (!effectiveBranchId) {
578
369
  setBranchResolutionError(new Error(`GET /api/sites/${siteId}/branches returned no usable branch. Check that the site id is correct and that the site has a main branch.`));
579
370
  }
580
- // Persisting an empty id would poison the next visit and build requests with a
581
- // blank branch segment; keep whatever is already selected instead.
582
371
  if (effectiveBranchId) {
583
372
  persistBranchId(effectiveBranchId);
584
373
  setBranchId(effectiveBranchId);
@@ -586,14 +375,8 @@ userRole = 'editor', children, }) {
586
375
  setCurrentBranch(branchList.find((b) => b.id === effectiveBranchId) ?? null);
587
376
  }
588
377
  catch (error) {
589
- // The editor cannot open a document without a branch, so this has to
590
- // reach the UI: swallowed, it leaves the editor on "Loading document"
591
- // forever with no indication of what was refused.
592
378
  setBranchResolutionError(describeRequestFailure(`GET /api/sites/${siteId}/branches`, error));
593
379
  console.error('Failed to load branches:', error);
594
- // Without a branch nothing else can load: useDocuments' fetch is branch-guarded, so
595
- // documentsLoading stays true forever and the editor sits blank. Say so rather than
596
- // leaving the user with an unexplained empty screen.
597
380
  if (showErrorNotifications && notificationContext?.addError) {
598
381
  const message = error instanceof Error ? error.message : String(error);
599
382
  notificationContext.addError(`Failed to load branches: ${message}`, () => void refreshBranchesRef.current?.());
@@ -603,8 +386,6 @@ userRole = 'editor', children, }) {
603
386
  setBranchesLoading(false);
604
387
  }
605
388
  }, [userClient, siteId, getPersistedBranchId, persistBranchId, showErrorNotifications, notificationContext]);
606
- // Lets the failure notification offer a retry without refreshBranches depending on
607
- // itself, the same ref idiom the template callbacks above use.
608
389
  const refreshBranchesRef = useRef(refreshBranches);
609
390
  refreshBranchesRef.current = refreshBranches;
610
391
  const createBranch = useCallback(async (name) => {
@@ -612,36 +393,28 @@ userRole = 'editor', children, }) {
612
393
  await refreshBranches();
613
394
  return branch;
614
395
  }, [userClient, siteId, refreshBranches]);
615
- // Initial branch load - only once
616
396
  useEffect(() => {
617
397
  if (!initializedRef.current) {
618
398
  initializedRef.current = true;
619
399
  void refreshBranches();
620
400
  }
621
401
  }, [refreshBranches]);
622
- // Perform save operation - uses refs to avoid dependency issues
623
402
  const performSave = useCallback(async (options) => {
624
403
  const dataToSave = pendingDataRef.current;
625
404
  const doc = currentDocumentRef.current;
626
405
  if (!dataToSave || !doc) {
627
406
  return;
628
407
  }
629
- // Data origin guard: reject writes if data doesn't belong to current document
630
408
  if (currentDataDocumentPathRef.current !== doc.path) {
631
409
  pendingDataRef.current = null;
632
410
  return;
633
411
  }
634
- // When realtime is connected, the DO handles persistence via WebSocket sync.
635
- // Skip REST API save to avoid creating duplicate versions without CRDT state.
636
412
  if (enableRealtime && realtimeConnectedRef.current) {
637
- // The data is already being sent via realtime.applyLocalChange in saveData.
638
- // Mark as saved since the DO will persist it.
639
413
  pendingDataRef.current = null;
640
414
  setSaveStatus('saved');
641
415
  setLastSaved(new Date());
642
416
  return;
643
417
  }
644
- // Fallback: REST API save when realtime is disabled or disconnected
645
418
  setSaveStatus('saving');
646
419
  setSaveError(null);
647
420
  try {
@@ -660,47 +433,33 @@ userRole = 'editor', children, }) {
660
433
  : userClient.versions.create(siteId, params));
661
434
  pendingActionsRef.current = [];
662
435
  }, { maxAttempts: maxRetries });
663
- // Note: We intentionally do NOT call setCurrentData(dataToSave) here.
664
- // The data is already in Puck's internal state (it came from Puck's onChange).
665
- // Updating currentData would trigger a re-render cascade that recreates the
666
- // p1Plugin, causing Puck to potentially re-render and flicker.
667
- // currentData should only be updated when loading a document or switching versions.
668
436
  pendingDataRef.current = null;
669
437
  setSaveStatus('saved');
670
438
  setLastSaved(new Date());
671
- // Keep the "latest" cache in sync with what was just persisted (REST
672
- // success = confirmed persistence) so returning from a historical preview
673
- // restores the correct content even if the on-return re-fetch fails.
674
439
  latestVersionDataRef.current = dataToSave;
675
440
  }
676
441
  catch (error) {
677
442
  setSaveStatus('error');
678
443
  const saveErr = error instanceof Error ? error : new Error(String(error));
679
444
  setSaveError(saveErr);
680
- // Show error notification with retry action
681
445
  if (showErrorNotifications) {
682
446
  notificationContext.addError(`Failed to save changes: ${saveErr.message}`, () => void performSave());
683
447
  }
684
448
  }
685
449
  }, [userClient, siteId, branchId, maxRetries, showErrorNotifications, notificationContext, enableRealtime]);
686
- // Debounced save
687
450
  const debouncedSave = useMemo(() => debounce(() => {
688
451
  void performSave();
689
452
  }, autoSaveDelay), [performSave, autoSaveDelay]);
690
- // Cleanup debounce on unmount
691
453
  useEffect(() => {
692
454
  return () => {
693
455
  debouncedSave.cancel();
694
456
  };
695
457
  }, [debouncedSave]);
696
- // Best-effort flush of a pending debounced save when the tab is closed,
697
- // navigated away from, or hidden.
698
458
  useEffect(() => {
699
459
  if (typeof window === 'undefined' || typeof document === 'undefined')
700
460
  return;
701
461
  const flushPendingSave = () => {
702
462
  if (pendingDataRef.current) {
703
- // keepalive lets the POST finish after the page is discarded.
704
463
  debouncedSave.cancel();
705
464
  void performSave({ keepalive: true });
706
465
  }
@@ -719,19 +478,14 @@ userRole = 'editor', children, }) {
719
478
  document.removeEventListener('visibilitychange', handleVisibilityChange);
720
479
  };
721
480
  }, [debouncedSave, performSave]);
722
- // Pause auto-save
723
481
  const pauseAutoSave = useCallback(() => {
724
482
  debouncedSave.pause();
725
483
  setAutoSavePaused(true);
726
484
  }, [debouncedSave]);
727
- // Resume auto-save
728
485
  const resumeAutoSave = useCallback(() => {
729
486
  debouncedSave.resume();
730
487
  setAutoSavePaused(false);
731
488
  }, [debouncedSave]);
732
- // Capture Puck action metadata from the onAction callback.
733
- // This captures the action type and relevant metadata fields so they
734
- // can be included in the sync payload for backend version storage.
735
489
  const handleAction = useCallback((action) => {
736
490
  const puckAction = {
737
491
  type: action.type || 'unknown',
@@ -756,28 +510,17 @@ userRole = 'editor', children, }) {
756
510
  pendingActionsRef.current = buffer.slice(-500);
757
511
  }
758
512
  }, []);
759
- // Public save function (triggers debounce)
760
- // Also resumes auto-save if paused, per user requirement
761
- // Sends changes via WebSocket when realtime is enabled (but not for remote updates)
762
513
  const saveData = useCallback((data) => {
763
514
  if (suppressNextSaveRef.current !== null) {
764
515
  const loadedSnapshot = suppressNextSaveRef.current;
765
516
  suppressNextSaveRef.current = null;
766
- // Drop only the echo; a structurally different first onChange is a real edit.
767
517
  if (isEqual(data, loadedSnapshot)) {
768
518
  return;
769
519
  }
770
520
  }
771
- // Only genuine user edits reach here — load/restore echoes arm
772
- // suppressNextSaveRef before they fire and are returned early above.
773
521
  if (!viewingVersionRef.current) {
774
522
  latestLocalDataRef.current = data;
775
523
  }
776
- // When realtime is enabled, detect whether this onChange came from a remote
777
- // sync (Yjs update from another client) vs. a local user edit.
778
- // Remote updates should NOT trigger a REST save — the DO handles persistence.
779
- // Important: we must NOT set pendingDataRef for remote syncs, otherwise
780
- // getHasUnsavedChanges() will incorrectly report unsaved changes.
781
524
  if (enableRealtime && realtime.connected) {
782
525
  if (pendingRemoteUpdatesRef.current > 0) {
783
526
  pendingRemoteUpdatesRef.current -= 1;
@@ -788,26 +531,16 @@ userRole = 'editor', children, }) {
788
531
  }
789
532
  else {
790
533
  const currentPath = currentDocumentRef.current?.path ?? null;
791
- // Data origin guard: verify the data in state belongs to the
792
- // current document. During rapid document switching, a stale
793
- // loadDocument call can resolve and set data from document Y
794
- // into state after currentDocument has been updated to X.
795
534
  const dataOriginPath = currentDataDocumentPathRef.current;
796
535
  if (dataOriginPath !== currentPath) {
797
536
  console.warn('[P1PuckProvider] saveData SKIPPED: data origin mismatch.', 'dataOrigin:', dataOriginPath, 'currentDoc:', currentPath);
798
537
  return;
799
538
  }
800
- // Connection identity guard: verify the realtime connection is
801
- // bound to the same document we're currently editing.
802
539
  const connectedPath = realtime.connectedDocumentPath;
803
540
  if (currentPath !== connectedPath) {
804
541
  console.warn('[P1PuckProvider] saveData SKIPPED: connection identity mismatch.', 'currentDoc:', currentPath, 'connectedDoc:', connectedPath);
805
542
  return;
806
543
  }
807
- // Local user edit — send via WebSocket (DO handles persistence)
808
- // Echo prevention is handled at lower layers:
809
- // - puckDataToYMap no-ops when Y.Doc already has identical data
810
- // - RealtimeClient.lastSentSnapshot drops sends matching last sent/received
811
544
  const actions = pendingActionsRef.current;
812
545
  if (actions.length > 0) {
813
546
  realtime.applyLocalChange(data, actions);
@@ -817,21 +550,11 @@ userRole = 'editor', children, }) {
817
550
  }
818
551
  trackSentData(data);
819
552
  pendingActionsRef.current = [];
820
- // Data sent via WebSocket — DO handles persistence.
821
- // Update save status directly (skip debouncedSave/performSave chain).
822
553
  setSaveStatus('saved');
823
554
  setLastSaved(new Date());
824
- // NOTE: intentionally do NOT update latestVersionDataRef here. This
825
- // is the optimistic local send, before the DO confirms persistence —
826
- // caching it would assert content that may never be saved. Realtime
827
- // returns use the live Yjs snapshot (or the server re-fetch), not
828
- // this cache (PCC-3421).
829
555
  return;
830
556
  }
831
557
  }
832
- // Mirror the realtime guard: don't save when previewing a historical version.
833
- // Without this, the PuckDataSynchronizer echo from loadVersion resumes the
834
- // debounce and writes historical data to the server.
835
558
  if (viewingVersionRef.current !== null) {
836
559
  return;
837
560
  }
@@ -842,15 +565,12 @@ userRole = 'editor', children, }) {
842
565
  }
843
566
  debouncedSave();
844
567
  }, [debouncedSave, enableRealtime, realtime.connected, trackSentData]);
845
- // Force immediate save
846
568
  const saveNow = useCallback(async () => {
847
569
  debouncedSave.cancel();
848
- // When realtime is connected, ensure the latest data is sent via WebSocket
849
570
  if (enableRealtime && realtimeConnectedRef.current && pendingDataRef.current) {
850
571
  const currentPath = currentDocumentRef.current?.path ?? null;
851
572
  const dataOriginPath = currentDataDocumentPathRef.current;
852
573
  const connectedPath = realtime.connectedDocumentPath;
853
- // Data origin + connection identity guards
854
574
  if (dataOriginPath === currentPath && currentPath === connectedPath) {
855
575
  realtime.applyLocalChange(pendingDataRef.current);
856
576
  pendingDataRef.current = null;
@@ -858,13 +578,9 @@ userRole = 'editor', children, }) {
858
578
  setLastSaved(new Date());
859
579
  return;
860
580
  }
861
- // Fall through to REST save if mismatch
862
581
  }
863
582
  await performSave();
864
583
  }, [debouncedSave, performSave, enableRealtime]);
865
- // Persist current in-memory edits as a REST version before a destructive operation.
866
- // Realtime: confirms WebSocket delivery then snapshots latestLocalDataRef as a version.
867
- // Non-realtime: delegates to performSave which creates a version from pendingDataRef.
868
584
  const persistCurrentEdits = useCallback(async () => {
869
585
  if (!enableRealtime || !realtimeConnectedRef.current) {
870
586
  await performSave();
@@ -890,55 +606,34 @@ userRole = 'editor', children, }) {
890
606
  }
891
607
  }
892
608
  }, [enableRealtime, realtime, performSave, userClient, siteId, branchId]);
893
- // Load document by path
894
609
  const loadDocument = useCallback(async (path) => {
895
- // Flush any pending debounced save while the outgoing document's
896
- // identity is still current, then drop the pending buffer so a late
897
- // debounce can never write one document's data into another.
898
610
  if (pendingDataRef.current) {
899
611
  debouncedSave.cancel();
900
612
  await performSave();
901
- // performSave clears the buffer only on success; if it survived, the
902
- // flush failed — abort the switch instead of discarding the edits.
903
613
  if (pendingDataRef.current) {
904
614
  return;
905
615
  }
906
616
  }
907
617
  debouncedSave.cancel();
908
618
  pendingDataRef.current = null;
909
- // Invalidate cached local edit so returnToLatest() doesn't treat it as unsaved.
910
619
  latestLocalDataRef.current = null;
911
- // Cancel any pending remote sync to prevent race conditions
912
- // where a stale remote update overrides the document we're loading
913
620
  cancelPendingRemoteSync();
914
- // Increment load request counter and capture for staleness detection.
915
- // If a newer loadDocument call starts before our awaits resolve, the
916
- // counter will have been incremented again and our captured value
917
- // will no longer match — signaling that our response is stale.
918
621
  loadRequestIdRef.current += 1;
919
622
  const thisRequestId = loadRequestIdRef.current;
920
623
  setDocumentLoading(true);
921
- // Pre-clear the current document so nothing can save against the old
922
- // one while the fetch is in flight. The canvas keeps showing the
923
- // previous document — documentLoading suppresses the empty state.
924
624
  commitCurrentData(null, null);
925
625
  try {
926
626
  const doc = await userClient.documents.getByPath(siteId, path.startsWith('/') ? path.slice(1) : path, branchId);
927
- // Staleness check: a newer loadDocument call has started
928
627
  if (thisRequestId !== loadRequestIdRef.current) {
929
628
  console.debug('[P1PuckProvider] Stale loadDocument — skipping (after getByPath)');
930
629
  return;
931
630
  }
932
- // Use ID not path — paths may differ by normalization, causing a false disconnect.
933
- // Same-doc keeps Yjs alive; cross-doc nulls out so useRealtime seeds a fresh Y.Doc.
934
631
  const isSameDocument = doc.id === currentDocumentRef.current?.id;
935
- // Null data before identity update so useRealtime sees initialData=null.
936
632
  commitCurrentData(null, null);
937
633
  if (!isSameDocument) {
938
634
  setCurrentDocument(null);
939
635
  }
940
636
  setCurrentDocument(doc);
941
- // Fetch template if document is bound to one (PROPOSAL-010)
942
637
  if (doc.templateId) {
943
638
  try {
944
639
  if (!userClient.templates) {
@@ -955,81 +650,47 @@ userRole = 'editor', children, }) {
955
650
  else {
956
651
  setCurrentTemplate(null);
957
652
  }
958
- // Get latest version
959
653
  const version = await userClient.versions.getLatest(siteId, branchId, doc.id);
960
654
  const puckData = snapshotToPuckData(version.snapshot);
961
- // Staleness check: a newer loadDocument call has started
962
655
  if (thisRequestId !== loadRequestIdRef.current) {
963
656
  console.debug('[P1PuckProvider] Stale loadDocument — skipping (after getLatest)');
964
657
  return;
965
658
  }
966
- // Mark REST data as non-local to prevent the Puck onChange handler
967
- // from echoing it back through the Y.Doc via applyLocalChange.
968
- // This is critical after the Hibernatable WS migration because
969
- // initializeIfNeeded() latency can invert the ordering of WebSocket
970
- // initial state vs REST response, causing the remote-update guard
971
- // (pendingRemoteUpdatesRef) to be bypassed.
972
659
  if (enableRealtime) {
973
660
  pendingRemoteUpdatesRef.current += 1;
974
- // Safety net: reset after React has processed the state updates.
975
- // If Puck's data is identical to current editor state, onChange
976
- // won't fire and the counter would never be decremented naturally.
977
- // Leaving it > 0 would cause subsequent local edits to be dropped.
978
661
  setTimeout(() => {
979
662
  pendingRemoteUpdatesRef.current = 0;
980
663
  }, 100);
981
664
  }
982
- // IMPORTANT: Update the ref BEFORE state updates
983
- // Setting to null allows remote updates after document loads
984
665
  viewingVersionRef.current = null;
985
- // Cancel any pending PuckDataCapture catch-up timer and reset its
986
- // ref before arming the data-origin guard below. Without this, the
987
- // timer can fire in the window between currentDataDocumentPathRef
988
- // being set to doc.path and the new document's first remote update
989
- // arriving — passing the origin check while still carrying the
990
- // previous document's Puck state.
991
666
  if (captureTimerRef.current) {
992
667
  clearTimeout(captureTimerRef.current);
993
668
  captureTimerRef.current = null;
994
669
  }
995
670
  realtimeDataCaptureRef.current = null;
996
671
  suppressNextSaveRef.current = puckData;
997
- // branchId is the branch this load requested, deliberately not
998
- // version.branchId: for an inherited document the backend serves main's
999
- // published version, so the version names main while the payload is what
1000
- // the requested branch should display. Recording main here would make
1001
- // every consumer reject the payload as foreign.
1002
672
  commitCurrentData(puckData, originFor(doc, branchId, version.id, false));
1003
673
  latestVersionDataRef.current = puckData;
1004
674
  setViewingVersion(null);
1005
- // Clear remoteSyncKey so document sync takes priority
1006
675
  setRemoteSyncKey(null);
1007
676
  pendingDataRef.current = null;
1008
677
  setSaveStatus('idle');
1009
678
  setSaveError(null);
1010
679
  }
1011
680
  catch (error) {
1012
- // Use warn, not error: callers handle this and console.error triggers
1013
- // the Next.js dev overlay unnecessarily.
1014
681
  console.warn('[P1PuckProvider] loadDocument failed:', error);
1015
- // Unload the current document so VersionBannerOverride shows the empty
1016
- // state instead of the previous document's content.
1017
682
  commitCurrentData(null, null);
1018
683
  setCurrentDocument(null);
1019
684
  throw error;
1020
685
  }
1021
686
  finally {
1022
- // Guarded so a stale call's completion can't clear the flag while a
1023
- // newer load is still in flight.
1024
687
  if (thisRequestId === loadRequestIdRef.current) {
1025
688
  setDocumentLoading(false);
1026
689
  }
1027
690
  }
1028
691
  }, [userClient, siteId, branchId, cancelPendingRemoteSync, enableRealtime, debouncedSave, performSave, commitCurrentData]);
1029
- // Load a specific version into the editor
692
+ loadDocForResetRef.current = loadDocument;
1030
693
  const loadVersion = useCallback(async (version) => {
1031
- // Cancel any pending remote sync to prevent race conditions
1032
- // where a stale remote update overrides the version we're loading
1033
694
  cancelPendingRemoteSync();
1034
695
  try {
1035
696
  const doc = currentDocumentRef.current;
@@ -1037,30 +698,19 @@ userRole = 'editor', children, }) {
1037
698
  throw new Error('No document loaded');
1038
699
  }
1039
700
  let versionToUse = version;
1040
- // If the version doesn't have snapshot data, try fetching it from the API
1041
701
  if (!version.snapshot || Object.keys(version.snapshot).length === 0) {
1042
702
  try {
1043
703
  versionToUse = await userClient.versions.get(siteId, branchId, doc.id, version.id);
1044
704
  }
1045
705
  catch (fetchError) {
1046
- // If fetching fails, log a warning and use the version as-is
1047
706
  console.warn('Could not fetch full version data, using version from list:', fetchError);
1048
707
  }
1049
708
  }
1050
- // Parses a JSON-string snapshot and falls back to blank Puck data for
1051
- // an empty/invalid snapshot (e.g. version 1's initial blank state).
1052
709
  const puckData = snapshotToPuckData(versionToUse.snapshot);
1053
- // IMPORTANT: Update the ref BEFORE state updates to block any incoming remote updates
1054
- // The effect that syncs viewingVersionRef runs AFTER render, which is too late
1055
- // Remote updates check this ref, so we need it set synchronously
1056
710
  viewingVersionRef.current = versionToUse;
1057
- // Historical: the document-sync plugin must not push this into the
1058
- // canvas that owns the live undo stack — the version bridge handles it.
1059
711
  commitCurrentData(puckData, originFor(doc, branchId, versionToUse.id, true));
1060
712
  setViewingVersion(versionToUse);
1061
- // Clear remoteSyncKey so version sync takes priority
1062
713
  setRemoteSyncKey(null);
1063
- // Pause auto-save when viewing historical version
1064
714
  debouncedSave.pause();
1065
715
  setAutoSavePaused(true);
1066
716
  pendingDataRef.current = null;
@@ -1071,13 +721,8 @@ userRole = 'editor', children, }) {
1071
721
  throw error;
1072
722
  }
1073
723
  }, [userClient, siteId, branchId, debouncedSave, cancelPendingRemoteSync, commitCurrentData]);
1074
- // Return to the latest version
1075
724
  const returnToLatest = useCallback(async () => {
1076
- // Cancel any pending remote sync to prevent race conditions
1077
- // The latest data will be loaded fresh, then remote sync can resume
1078
725
  cancelPendingRemoteSync();
1079
- // Get current state from Yjs if realtime is connected
1080
- // This ensures we get any changes made by other users while viewing history
1081
726
  let dataToRestore = null;
1082
727
  if (enableRealtime && realtime.connected) {
1083
728
  const currentYjsData = realtime.getSnapshot();
@@ -1085,20 +730,12 @@ userRole = 'editor', children, }) {
1085
730
  dataToRestore = currentYjsData;
1086
731
  }
1087
732
  }
1088
- // 1. Edits typed inside the debounce window — cleared from pendingDataRef by loadVersion but still live here.
1089
733
  const hadLocalEdits = !!latestLocalDataRef.current;
1090
734
  dataToRestore ??= latestLocalDataRef.current;
1091
- latestLocalDataRef.current = null; // consume — prevents stale ref short-circuiting server fetch on next history visit
1092
- // Re-fetch the true latest from the server when no live Yjs snapshot is
1093
- // available. The cached latestVersionDataRef is only a last-resort fallback
1094
- // (e.g. no current document); it is NOT used on fetch failure — silently
1095
- // editing on a possibly-stale snapshot and resuming autosave is the exact
1096
- // data-loss path from PCC-3421. On failure we notify and abort instead,
1097
- // keeping the user in read-only preview so nothing is overwritten.
735
+ latestLocalDataRef.current = null;
1098
736
  if (!dataToRestore) {
1099
737
  const doc = currentDocumentRef.current;
1100
738
  if (doc) {
1101
- // 2. Authoritative server state — the only source that reflects saves from other tabs, users, or agents.
1102
739
  setIsReturningToLatest(true);
1103
740
  try {
1104
741
  const version = await userClient.versions.getLatest(siteId, branchId, doc.id);
@@ -1116,16 +753,10 @@ userRole = 'editor', children, }) {
1116
753
  setIsReturningToLatest(false);
1117
754
  }
1118
755
  }
1119
- // 3. No document context — can't fetch; use last value written by performSave.
1120
756
  dataToRestore ??= latestVersionDataRef.current;
1121
757
  }
1122
758
  if (dataToRestore) {
1123
- // IMPORTANT: Update the ref BEFORE state updates
1124
- // Setting to null allows remote updates to resume
1125
759
  viewingVersionRef.current = null;
1126
- // Suppress the PuckDataSynchronizer echo: setCurrentData triggers a setData
1127
- // dispatch and Puck fires onChange → saveData. Arm the ref so saveData drops
1128
- // that echo; we control the save explicitly below.
1129
760
  suppressNextSaveRef.current = dataToRestore;
1130
761
  if (enableRealtime) {
1131
762
  pendingRemoteUpdatesRef.current += 1;
@@ -1136,14 +767,9 @@ userRole = 'editor', children, }) {
1136
767
  commitCurrentData(dataToRestore, originFor(currentDocumentRef.current, branchId, null, false));
1137
768
  latestVersionDataRef.current = dataToRestore;
1138
769
  setViewingVersion(null);
1139
- // Clear remoteSyncKey so latest version sync takes priority
1140
770
  setRemoteSyncKey(null);
1141
- // Resume auto-save
1142
771
  debouncedSave.resume();
1143
772
  setAutoSavePaused(false);
1144
- // Only queue a save when restoring from local in-memory edits — those were
1145
- // never persisted. Data from the server fetch is already the latest saved
1146
- // version; queuing a save would create a spurious identical version.
1147
773
  if (hadLocalEdits) {
1148
774
  pendingDataRef.current = dataToRestore;
1149
775
  debouncedSave();
@@ -1151,12 +777,7 @@ userRole = 'editor', children, }) {
1151
777
  setSaveStatus('idle');
1152
778
  }
1153
779
  }, [debouncedSave, cancelPendingRemoteSync, enableRealtime, realtime, userClient, siteId, branchId, showErrorNotifications, notificationContext, commitCurrentData]);
1154
- // Computed property for whether viewing historical version
1155
780
  const isViewingHistoricalVersion = viewingVersion !== null;
1156
- // =========================================================================
1157
- // Content Type Templates: Permission Resolution (PROPOSAL-010)
1158
- // =========================================================================
1159
- // Resolve template: either from document binding or registry path
1160
781
  const resolvedTemplate = useMemo(() => {
1161
782
  if (currentTemplate)
1162
783
  return currentTemplate;
@@ -1168,17 +789,12 @@ userRole = 'editor', children, }) {
1168
789
  return null;
1169
790
  return branchTemplates.find((t) => t.name === match[1]) ?? null;
1170
791
  }, [currentTemplate, currentDocument?.path, branchTemplates]);
1171
- // Create Puck permissions resolver based on current template and user role
1172
792
  const resolvePermissions = useMemo(() => createPuckPermissions(resolvedTemplate, userRole, isViewingHistoricalVersion), [resolvedTemplate, userRole, isViewingHistoricalVersion]);
1173
- // Show notification when template list fails to load
1174
793
  useEffect(() => {
1175
794
  if (templatesError && showErrorNotifications && notificationContext?.addError) {
1176
795
  notificationContext.addError(`Failed to load templates: ${templatesError.message}`, () => void refreshTemplates());
1177
796
  }
1178
797
  }, [templatesError]);
1179
- // =========================================================================
1180
- // Content Type Templates: Re-fetch template on tab focus (CUJ-15a)
1181
- // =========================================================================
1182
798
  useEffect(() => {
1183
799
  if (typeof document === 'undefined')
1184
800
  return;
@@ -1197,37 +813,26 @@ userRole = 'editor', children, }) {
1197
813
  });
1198
814
  }
1199
815
  catch {
1200
- // Silently ignore — stale template is better than no template
1201
816
  }
1202
817
  };
1203
818
  document.addEventListener('visibilitychange', handler);
1204
819
  return () => document.removeEventListener('visibilitychange', handler);
1205
820
  }, [siteId, userClient]);
1206
- // =========================================================================
1207
- // Phase 9: Presence & Agent Mode State
1208
- // =========================================================================
1209
- // Presence state (when presenceEnabled)
1210
821
  const [presenceActors, setPresenceActors] = useState([]);
1211
822
  const presenceInitializedRef = useRef(false);
1212
- // Track previous actors for onPresenceChange callback comparison
1213
823
  const prevActorsRef = useRef([]);
1214
- // Conflict notifications state
1215
824
  const [conflicts, setConflicts] = useState([]);
1216
- // Agent edit session state (when agentModeEnabled && agentId)
1217
825
  const [agentSession, setAgentSession] = useState(null);
1218
826
  const [agentEditLoading, setAgentEditLoading] = useState(false);
1219
827
  const [agentEditError, setAgentEditError] = useState(null);
1220
- // Fetch presence data
1221
828
  const fetchPresence = useCallback(async () => {
1222
829
  if (!presenceEnabled || !branchId)
1223
830
  return;
1224
831
  try {
1225
832
  const branchPresence = await userClient.presence.getBranchPresence(siteId, branchId);
1226
- // Filter out self and enrich with names
1227
833
  const filteredActors = branchPresence.actors.filter((actor) => actor.actorId !== userId);
1228
834
  const enrichedActors = enrichActorsWithNames(filteredActors);
1229
835
  setPresenceActors(enrichedActors);
1230
- // Call onPresenceChange if actors changed
1231
836
  if (onPresenceChange) {
1232
837
  const actorsChanged = JSON.stringify(enrichedActors.map((a) => a.id).sort()) !==
1233
838
  JSON.stringify(prevActorsRef.current.map((a) => a.id).sort());
@@ -1241,27 +846,19 @@ userRole = 'editor', children, }) {
1241
846
  console.error('Failed to fetch presence:', error);
1242
847
  }
1243
848
  }, [presenceEnabled, branchId, siteId, userId, userClient, onPresenceChange, enrichActorsWithNames]);
1244
- // Keep fetchPresence in a ref to avoid restarting the interval when callback changes
1245
849
  const fetchPresenceRef = useRef(fetchPresence);
1246
850
  useEffect(() => {
1247
851
  fetchPresenceRef.current = fetchPresence;
1248
852
  }, [fetchPresence]);
1249
- // Initial presence fetch and polling
1250
- // HTTP polling is skipped when WebSocket presence is active and connected
1251
853
  useEffect(() => {
1252
854
  if (!presenceEnabled)
1253
855
  return;
1254
- // Skip HTTP polling if WebSocket presence is handling updates
1255
856
  const shouldSkipPolling = wsPresenceActive && realtime.connected;
1256
- // Initial fetch (only if WS isn't active yet)
1257
857
  if (!presenceInitializedRef.current && !shouldSkipPolling) {
1258
858
  presenceInitializedRef.current = true;
1259
859
  void fetchPresenceRef.current();
1260
860
  }
1261
- // Set up polling - use ref to avoid restarting interval when callback changes
1262
- // Skip polling when WebSocket is handling presence
1263
861
  const intervalId = setInterval(() => {
1264
- // Check again at each interval - WS state may have changed
1265
862
  if (!wsPresenceActive || !realtime.connected) {
1266
863
  void fetchPresenceRef.current();
1267
864
  }
@@ -1270,29 +867,21 @@ userRole = 'editor', children, }) {
1270
867
  clearInterval(intervalId);
1271
868
  };
1272
869
  }, [presenceEnabled, presencePollingInterval, realtime.connected]);
1273
- // Reset presence when disabled
1274
870
  useEffect(() => {
1275
871
  if (!presenceEnabled) {
1276
872
  setPresenceActors([]);
1277
873
  presenceInitializedRef.current = false;
1278
874
  }
1279
875
  }, [presenceEnabled]);
1280
- // Reset WebSocket presence state when disconnected so the UI falls back
1281
- // to HTTP-polled presence data. Without this, wsPresenceActive stays
1282
- // true after disconnect and the UI shows stale WebSocket presence
1283
- // (e.g. an agent that already completed its edit session still appears).
1284
876
  useEffect(() => {
1285
877
  if (!realtime.connected) {
1286
878
  setWsPresenceActive(false);
1287
879
  setWsPresenceActors([]);
1288
880
  }
1289
881
  }, [realtime.connected]);
1290
- // Compute derived presence values
1291
- // Prefer WebSocket presence when connected for instant updates
1292
882
  const presenceState = useMemo(() => {
1293
883
  if (!presenceEnabled)
1294
884
  return null;
1295
- // Use WebSocket presence when active and connected, otherwise fall back to HTTP polling data
1296
885
  const effectiveActors = wsPresenceActive && realtime.connected ? wsPresenceActors : presenceActors;
1297
886
  const humans = effectiveActors.filter((actor) => actor.role === 'human');
1298
887
  const agents = effectiveActors.filter((actor) => actor.role === 'agent');
@@ -1307,16 +896,10 @@ userRole = 'editor', children, }) {
1307
896
  refresh: fetchPresence,
1308
897
  };
1309
898
  }, [presenceEnabled, presenceActors, wsPresenceActors, wsPresenceActive, realtime.connected, fetchPresence]);
1310
- // Keep presence in a ref so it can be read via getter without triggering
1311
- // context recreation. Presence changes frequently (focus region broadcasts)
1312
- // but shouldn't cause PuckDataSynchronizer or plugin re-renders.
1313
899
  const presenceStateRef = useRef(presenceState);
1314
900
  presenceStateRef.current = presenceState;
1315
901
  const humanPresenceKey = presenceIdentityKey(presenceState?.humans);
1316
902
  const agentPresenceKey = presenceIdentityKey(presenceState?.agents);
1317
- // =========================================================================
1318
- // Phase 9: Agent Edit Capabilities (when this client IS an agent)
1319
- // =========================================================================
1320
903
  const agentEditCapabilities = useMemo(() => {
1321
904
  if (!agentModeEnabled || !agentId)
1322
905
  return null;
@@ -1423,11 +1006,7 @@ userRole = 'editor', children, }) {
1423
1006
  agentEditLoading,
1424
1007
  agentEditError,
1425
1008
  ]);
1426
- // =========================================================================
1427
- // Phase 9: Agent Trigger (for human users to trigger agents)
1428
- // =========================================================================
1429
1009
  const triggerAgentFn = useMemo(() => {
1430
- // Only available when agentModeEnabled but no agentId (human user)
1431
1010
  if (!agentModeEnabled || agentId)
1432
1011
  return null;
1433
1012
  const documentPath = currentDocument?.path ?? null;
@@ -1436,7 +1015,6 @@ userRole = 'editor', children, }) {
1436
1015
  return { success: false, error: 'No document loaded' };
1437
1016
  }
1438
1017
  try {
1439
- // Check permission
1440
1018
  const permission = await userClient.agentEdit.canEdit(siteId, branchId, documentPath, {
1441
1019
  agentId: action.agentId,
1442
1020
  trigger: 'human_requested',
@@ -1447,7 +1025,6 @@ userRole = 'editor', children, }) {
1447
1025
  if (!permission.allowed) {
1448
1026
  return { success: false, error: permission.reason };
1449
1027
  }
1450
- // Start edit session
1451
1028
  const session = await userClient.agentEdit.startEdit(siteId, branchId, documentPath, {
1452
1029
  agentId: action.agentId,
1453
1030
  trigger: 'human_requested',
@@ -1465,13 +1042,9 @@ userRole = 'editor', children, }) {
1465
1042
  }
1466
1043
  };
1467
1044
  }, [agentModeEnabled, agentId, currentDocument, siteId, branchId, userId, userClient]);
1468
- // =========================================================================
1469
- // Phase 9: Conflict Notifications
1470
- // =========================================================================
1471
1045
  const dismissConflict = useCallback((id) => {
1472
1046
  setConflicts((prev) => prev.filter((c) => c.id !== id));
1473
1047
  }, []);
1474
- // Stop an agent's edit session (human-initiated)
1475
1048
  const handleStopAgent = useCallback(async (agent) => {
1476
1049
  const documentPath = currentDocumentRef.current?.path;
1477
1050
  if (!documentPath) {
@@ -1481,7 +1054,6 @@ userRole = 'editor', children, }) {
1481
1054
  try {
1482
1055
  await userClient.agentEdit.stopAgent(siteId, branchId, documentPath, agent.actorId);
1483
1056
  notificationContext.addSuccess(`Agent "${agent.name}" has been stopped`);
1484
- // Refresh presence to reflect the agent's removal
1485
1057
  if (presenceEnabled) {
1486
1058
  void fetchPresenceRef.current();
1487
1059
  }
@@ -1491,12 +1063,9 @@ userRole = 'editor', children, }) {
1491
1063
  notificationContext.addError(`Failed to stop agent: ${message}`);
1492
1064
  }
1493
1065
  }, [userClient, siteId, branchId, notificationContext, presenceEnabled]);
1494
- // Create checkpoint
1495
1066
  const createCheckpoint = useCallback(async (name) => {
1496
- // Save any pending changes first
1497
1067
  if (pendingDataRef.current) {
1498
1068
  if (enableRealtime && realtimeConnectedRef.current) {
1499
- // Data origin guard: only send if data belongs to current document
1500
1069
  const currentPath = currentDocumentRef.current?.path ?? null;
1501
1070
  if (currentDataDocumentPathRef.current === currentPath) {
1502
1071
  realtime.applyLocalChange(pendingDataRef.current);
@@ -1508,8 +1077,6 @@ userRole = 'editor', children, }) {
1508
1077
  await performSave();
1509
1078
  }
1510
1079
  }
1511
- // When realtime is connected, wait for the DO to acknowledge receipt of
1512
- // all preceding WebSocket messages before creating the checkpoint.
1513
1080
  if (enableRealtime && realtimeConnectedRef.current) {
1514
1081
  try {
1515
1082
  await realtime.waitForDelivery();
@@ -1525,13 +1092,11 @@ userRole = 'editor', children, }) {
1525
1092
  });
1526
1093
  return checkpoint;
1527
1094
  }, [userClient, siteId, branchId, debouncedSave, performSave, enableRealtime, realtime]);
1528
- // Publish current document
1529
1095
  const publishDocument = useCallback(async () => {
1530
1096
  const doc = currentDocumentRef.current;
1531
1097
  if (!doc) {
1532
1098
  throw new Error('No document loaded to publish');
1533
1099
  }
1534
- // Send any pending changes before publish
1535
1100
  if (pendingDataRef.current) {
1536
1101
  if (enableRealtime && realtimeConnectedRef.current) {
1537
1102
  const currentPath = currentDocumentRef.current?.path ?? null;
@@ -1545,10 +1110,6 @@ userRole = 'editor', children, }) {
1545
1110
  await performSave();
1546
1111
  }
1547
1112
  }
1548
- // When realtime is connected, use WebSocket-driven publish.
1549
- // The DO handles the entire flow: flush CRDT to Postgres, then
1550
- // create the checkpoint. TCP ordering guarantees all preceding
1551
- // binary CRDT updates are processed before the publish_request.
1552
1113
  if (enableRealtime && realtimeConnectedRef.current) {
1553
1114
  const publishResult = await realtime.requestPublish();
1554
1115
  if (!publishResult.success) {
@@ -1559,61 +1120,29 @@ userRole = 'editor', children, }) {
1559
1120
  }
1560
1121
  return publishResult.checkpoint;
1561
1122
  }
1562
- // Fallback: HTTP publish when realtime is not connected
1563
1123
  const result = await userClient.documents.publish(siteId, branchId, doc.id);
1564
1124
  return result.checkpoint;
1565
1125
  }, [userClient, siteId, branchId, debouncedSave, performSave, enableRealtime, realtime]);
1566
- // Tracks the currently in-flight switchBranch flush (everything after the
1567
- // synchronous branch-selection commit), so a rapid re-selection can await
1568
- // the prior call instead of racing it. See switchBranch below.
1569
1126
  const switchBranchInFlightRef = useRef(null);
1570
- // Switch branch
1571
1127
  const switchBranch = useCallback(async (newBranchId) => {
1572
- // Commit the new workstream selection FIRST, synchronously, before the
1573
- // async save-flush below. The UI (WorkstreamSwitcher) fires this inside
1574
- // startTransition without awaiting it, so its "Switching..." busy state
1575
- // clears as soon as this function's synchronous portion returns — well
1576
- // before a save-flush of the outgoing document would resolve. If the
1577
- // user navigates to a different document in that window, this provider
1578
- // can remount (the editor route has no layout.tsx to shield it across
1579
- // param changes), and its branchId initializer re-reads sessionStorage.
1580
- // Persisting here — before the flush, not after — guarantees that read
1581
- // always sees the newly selected branch instead of a stale one,
1582
- // preventing the workstream selector from snapping back to Live/main.
1583
1128
  setBranchId(newBranchId);
1584
1129
  persistBranchId(newBranchId);
1585
1130
  const branch = branches.find((b) => b.id === newBranchId);
1586
1131
  setCurrentBranch(branch ?? null);
1587
- // Serialize the flush portion below against any switchBranch call
1588
- // still in flight. Because the branch commit above is synchronous but
1589
- // the flush is not, a rapid second call (the switcher fires onSwitch
1590
- // fire-and-forget, so its busy state clears well before the flush
1591
- // resolves) can otherwise start its own flush while pendingDataRef
1592
- // still holds the first call's outgoing edit — grabbing a performSave
1593
- // closure already pointed at the intermediate branch and saving that
1594
- // same edit a second time, to the wrong branch. Awaiting the prior
1595
- // in-flight flush first guarantees only one flush ever reads/clears
1596
- // pendingDataRef at a time, and that each flush's performSave targets
1597
- // the branch that was current when that flush actually started.
1598
1132
  const previousSwitch = switchBranchInFlightRef.current;
1599
1133
  const thisSwitch = (async () => {
1600
1134
  if (previousSwitch) {
1601
- // A prior switch's flush failure shouldn't block this one.
1602
1135
  await previousSwitch.catch(() => undefined);
1603
1136
  }
1604
- // Cancel any pending remote sync - we're switching to a different branch
1605
1137
  cancelPendingRemoteSync();
1606
- // Save any pending changes on the outgoing document/branch first
1607
1138
  if (pendingDataRef.current) {
1608
1139
  debouncedSave.cancel();
1609
1140
  await performSave();
1610
1141
  }
1611
- // IMPORTANT: Update the ref BEFORE state updates
1612
1142
  viewingVersionRef.current = null;
1613
1143
  setCurrentDocument(null);
1614
1144
  commitCurrentData(null, null);
1615
1145
  setViewingVersion(null);
1616
- // Clear remoteSyncKey so new branch document sync takes priority
1617
1146
  setRemoteSyncKey(null);
1618
1147
  pendingDataRef.current = null;
1619
1148
  setSaveStatus('idle');
@@ -1629,7 +1158,6 @@ userRole = 'editor', children, }) {
1629
1158
  }
1630
1159
  }
1631
1160
  }, [branches, debouncedSave, performSave, cancelPendingRemoteSync, persistBranchId, commitCurrentData]);
1632
- // Presence context value (for hooks like useFocusRegionReporting)
1633
1161
  const presenceContextValue = useMemo(() => {
1634
1162
  if (!presenceEnabled)
1635
1163
  return null;
@@ -1649,8 +1177,6 @@ userRole = 'editor', children, }) {
1649
1177
  ?.filter(a => a.state === 'editing')
1650
1178
  ?.flatMap(a => a.focusRegions ?? []) ?? [],
1651
1179
  isAgentEditing: presenceState?.hasActiveAgents ?? false,
1652
- // Agent edit functions - placeholder implementations
1653
- // These are handled by agentEditCapabilities in the main context
1654
1180
  canEdit: async () => ({ allowed: true }),
1655
1181
  startEdit: async () => ({ sessionId: '' }),
1656
1182
  completeEdit: async () => ({ success: true }),
@@ -1667,13 +1193,6 @@ userRole = 'editor', children, }) {
1667
1193
  realtime.connected,
1668
1194
  presenceState,
1669
1195
  ]);
1670
- // =========================================================================
1671
- // Stable Callback Wrappers
1672
- // =========================================================================
1673
- // Wrap volatile callbacks in refs + stable wrappers so consumers don't need
1674
- // to stabilize them manually. The ref is updated inline during render (safe
1675
- // because it's a ref write, not a state write) and the stable wrapper
1676
- // delegates to ref.current, ensuring the latest implementation is always used.
1677
1196
  const saveDataRef = useRef(saveData);
1678
1197
  saveDataRef.current = saveData;
1679
1198
  const stableSaveData = useCallback((data) => saveDataRef.current(data), []);
@@ -1698,9 +1217,8 @@ userRole = 'editor', children, }) {
1698
1217
  const switchBranchRef = useRef(switchBranch);
1699
1218
  switchBranchRef.current = switchBranch;
1700
1219
  const stableSwitchBranch = useCallback((newBranchId) => switchBranchRef.current(newBranchId), []);
1701
- // Get pending actions for forwarding to backend
1702
1220
  const getPendingActions = useCallback(() => {
1703
- return [...pendingActionsRef.current]; // Return copy to prevent external mutation
1221
+ return [...pendingActionsRef.current];
1704
1222
  }, []);
1705
1223
  const loadDocumentRef = useRef(loadDocument);
1706
1224
  loadDocumentRef.current = loadDocument;
@@ -1714,10 +1232,6 @@ userRole = 'editor', children, }) {
1714
1232
  const handleStopAgentRef = useRef(handleStopAgent);
1715
1233
  handleStopAgentRef.current = handleStopAgent;
1716
1234
  const stableStopAgent = useCallback((agent) => handleStopAgentRef.current(agent), []);
1717
- // =========================================================================
1718
- // Stable Getters (Items 2 & 3)
1719
- // =========================================================================
1720
- // Refs updated inline during render, exposed as stable getter callbacks.
1721
1235
  const saveStatusGetterRef = useRef(saveStatus);
1722
1236
  saveStatusGetterRef.current = saveStatus;
1723
1237
  const getSaveStatus = useCallback(() => saveStatusGetterRef.current, []);
@@ -1728,7 +1242,6 @@ userRole = 'editor', children, }) {
1728
1242
  saveErrorGetterRef.current = saveError;
1729
1243
  const getSaveError = useCallback(() => saveErrorGetterRef.current, []);
1730
1244
  const getHasUnsavedChanges = useCallback(() => pendingDataRef.current !== null, []);
1731
- // Data sync getters (Item 3)
1732
1245
  const currentDataGetterRef = useRef(currentData);
1733
1246
  currentDataGetterRef.current = currentData;
1734
1247
  const remoteSyncKeyGetterRef = useRef(remoteSyncKey);
@@ -1750,9 +1263,6 @@ userRole = 'editor', children, }) {
1750
1263
  return `doc-${doc.id}-latest`;
1751
1264
  return undefined;
1752
1265
  }, []);
1753
- // =========================================================================
1754
- // Plugin Composition (B.4)
1755
- // =========================================================================
1756
1266
  const resolvedFeatureConfig = useMemo(() => {
1757
1267
  if (featureConfig)
1758
1268
  return resolveFeatureConfig(featureConfig);
@@ -1763,7 +1273,7 @@ userRole = 'editor', children, }) {
1763
1273
  });
1764
1274
  }, [featureConfig, enableRealtime, presenceEnabled, agentModeEnabled]);
1765
1275
  const activePlugins = useMemo(() => {
1766
- const plugins = featurePlugins ?? DEFAULT_CSS_FEATURE_PLUGINS;
1276
+ const plugins = featurePlugins ?? DEFAULT_CCR_FEATURE_PLUGINS;
1767
1277
  return resolveActivePlugins(plugins, resolvedFeatureConfig);
1768
1278
  }, [featurePlugins, resolvedFeatureConfig]);
1769
1279
  const pluginDeps = useMemo(() => ({
@@ -1774,16 +1284,12 @@ userRole = 'editor', children, }) {
1774
1284
  config: resolvedFeatureConfig,
1775
1285
  }), [userClient, siteId, branchId, userId, resolvedFeatureConfig]);
1776
1286
  const ComposedPluginProviders = useMemo(() => composeProviders(activePlugins, resolvedFeatureConfig, pluginDeps), [activePlugins, resolvedFeatureConfig, pluginDeps]);
1777
- // =========================================================================
1778
- // safeData (Item 4) — never null
1779
- // =========================================================================
1780
1287
  const EMPTY_PUCK_DATA = useMemo(() => ({ content: [], root: { props: {} } }), []);
1781
1288
  const lastGoodDataRef = useRef(EMPTY_PUCK_DATA);
1782
1289
  if (currentDocument && currentData) {
1783
1290
  lastGoodDataRef.current = currentData;
1784
1291
  }
1785
1292
  const safeData = lastGoodDataRef.current;
1786
- // Context value
1787
1293
  const contextValue = useMemo(() => ({
1788
1294
  client: userClient,
1789
1295
  notifications: notificationContext,
@@ -1805,16 +1311,13 @@ userRole = 'editor', children, }) {
1805
1311
  createCheckpoint: stableCreateCheckpoint,
1806
1312
  publishDocument: stablePublishDocument,
1807
1313
  switchBranch: stableSwitchBranch,
1808
- // Stable getters (Items 2, 3)
1809
1314
  getSaveStatus,
1810
1315
  getLastSaved,
1811
1316
  getSaveError,
1812
1317
  getHasUnsavedChanges,
1813
1318
  getSyncData,
1814
1319
  getDataSyncKey,
1815
- // safeData (Item 4)
1816
1320
  safeData,
1817
- // Documents (Item 8)
1818
1321
  documents: branchDocuments,
1819
1322
  documentsLoading,
1820
1323
  refreshDocuments,
@@ -1833,9 +1336,6 @@ userRole = 'editor', children, }) {
1833
1336
  pauseAutoSave: stablePauseAutoSave,
1834
1337
  resumeAutoSave: stableResumeAutoSave,
1835
1338
  viewingVersion,
1836
- // Exposed for type/back-compat only — nothing renders from it. Read from
1837
- // the ref; the memo already recomputes each save (via lastSaved), so this
1838
- // stays adequately fresh without being a reactive dependency.
1839
1339
  latestVersionData: latestVersionDataRef.current,
1840
1340
  isViewingHistoricalVersion,
1841
1341
  isReturningToLatest,
@@ -1844,16 +1344,9 @@ userRole = 'editor', children, }) {
1844
1344
  realtimeEnabled: enableRealtime,
1845
1345
  realtimeConnected: realtime.connected,
1846
1346
  remoteSyncKey,
1847
- // WebSocket presence - send focus regions via WebSocket when connected
1848
1347
  sendFocusRegions: realtime.sendFocusRegions,
1849
- // Puck action metadata capture - pass as onAction to <Puck>
1850
1348
  handleAction,
1851
- // Get pending actions for backend forwarding (PROPOSAL-010)
1852
1349
  getPendingActions,
1853
- // Phase 9: Presence & Agent values
1854
- // Use getter to avoid context recreation on every focus-region update.
1855
- // Expose humanPresenceCount/hasActiveHumans/hasActiveAgents directly so every
1856
- // join/leave triggers context re-renders and causes consumers to re-invoke the getter.
1857
1350
  get presence() { return presenceStateRef.current; },
1858
1351
  hasActiveHumans: presenceState?.hasActiveHumans ?? false,
1859
1352
  humanPresenceCount: presenceState?.humans.length ?? 0,
@@ -1863,12 +1356,9 @@ userRole = 'editor', children, }) {
1863
1356
  stopAgent: stableStopAgent,
1864
1357
  conflicts,
1865
1358
  dismissConflict,
1866
- // Feature configuration (Phase B.5)
1867
1359
  featureConfig: resolvedFeatureConfig,
1868
- // Internal: realtime data capture for catch-up (sends missed keystrokes)
1869
1360
  _realtimeDataCaptureRef: enableRealtime ? realtimeDataCaptureRef : null,
1870
1361
  _onRealtimeDataCapture: enableRealtime ? handleRealtimeDataCapture : null,
1871
- // Content Type Templates (PROPOSAL-010)
1872
1362
  userRole,
1873
1363
  templates: branchTemplates,
1874
1364
  templatesLoading,
@@ -1930,11 +1420,6 @@ userRole = 'editor', children, }) {
1930
1420
  handleAction,
1931
1421
  getPendingActions,
1932
1422
  handleRealtimeDataCapture,
1933
- // Phase 9 dependencies (full presenceState excluded — accessed via getter/ref,
1934
- // but humanPresenceCount/hasActiveHumans/hasActiveAgents are direct values so every
1935
- // join/leave triggers re-renders regardless of how many actors are present).
1936
- // The roster keys cover the changes a count cannot see: a same-frame join/leave of
1937
- // two humans would keep the count the same but change the roster.
1938
1423
  humanPresenceKey,
1939
1424
  agentPresenceKey,
1940
1425
  presenceState?.humans.length,
@@ -1947,7 +1432,6 @@ userRole = 'editor', children, }) {
1947
1432
  dismissConflict,
1948
1433
  enableRealtime,
1949
1434
  resolvedFeatureConfig,
1950
- // Content Type Templates
1951
1435
  userRole,
1952
1436
  branchTemplates,
1953
1437
  templatesLoading,
@@ -1956,7 +1440,6 @@ userRole = 'editor', children, }) {
1956
1440
  currentTemplate,
1957
1441
  resolvePermissions,
1958
1442
  ]);
1959
- // Wrap with PresenceContext.Provider when presence is enabled
1960
1443
  const wrappedChildren = presenceContextValue ? (_jsx(PresenceContext.Provider, { value: presenceContextValue, children: children })) : (children);
1961
1444
  return (_jsx(P1PuckContext.Provider, { value: contextValue, children: _jsx(ComposedPluginProviders, { children: wrappedChildren }) }));
1962
1445
  }