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