@pantheon-systems/puck-css 0.4.2 → 0.4.4

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 (202) hide show
  1. package/dist/core/config.d.ts +3 -0
  2. package/dist/core/config.d.ts.map +1 -1
  3. package/dist/core/config.js +5 -6
  4. package/dist/core/config.js.map +1 -1
  5. package/dist/core/featureConfig.d.ts +2 -0
  6. package/dist/core/featureConfig.d.ts.map +1 -1
  7. package/dist/core/featureConfig.js +4 -0
  8. package/dist/core/featureConfig.js.map +1 -1
  9. package/dist/core/types.d.ts +58 -1
  10. package/dist/core/types.d.ts.map +1 -1
  11. package/dist/data/dal/index.d.ts +5 -1
  12. package/dist/data/dal/index.d.ts.map +1 -1
  13. package/dist/data/dal/index.js +22 -1
  14. package/dist/data/dal/index.js.map +1 -1
  15. package/dist/data/dal/init.d.ts.map +1 -1
  16. package/dist/data/dal/init.js +22 -14
  17. package/dist/data/dal/init.js.map +1 -1
  18. package/dist/data/dal/p1-store.d.ts +15 -1
  19. package/dist/data/dal/p1-store.d.ts.map +1 -1
  20. package/dist/data/dal/p1-store.js +51 -5
  21. package/dist/data/dal/p1-store.js.map +1 -1
  22. package/dist/data/dal/types.d.ts +13 -1
  23. package/dist/data/dal/types.d.ts.map +1 -1
  24. package/dist/data/page-store.d.ts +17 -2
  25. package/dist/data/page-store.d.ts.map +1 -1
  26. package/dist/data/page-store.js +47 -5
  27. package/dist/data/page-store.js.map +1 -1
  28. package/dist/data/route-templates.js +1 -1
  29. package/dist/data/route-templates.js.map +1 -1
  30. package/dist/editor/P1App.d.ts.map +1 -1
  31. package/dist/editor/P1App.js +1 -1
  32. package/dist/editor/P1App.js.map +1 -1
  33. package/dist/editor/P1PuckProvider.d.ts.map +1 -1
  34. package/dist/editor/P1PuckProvider.js +175 -66
  35. package/dist/editor/P1PuckProvider.js.map +1 -1
  36. package/dist/editor/plugin/P1Plugin.d.ts +16 -1
  37. package/dist/editor/plugin/P1Plugin.d.ts.map +1 -1
  38. package/dist/editor/plugin/P1Plugin.js +35 -4
  39. package/dist/editor/plugin/P1Plugin.js.map +1 -1
  40. package/dist/editor/plugin/createP1Overrides.d.ts.map +1 -1
  41. package/dist/editor/plugin/createP1Overrides.js +4 -1
  42. package/dist/editor/plugin/createP1Overrides.js.map +1 -1
  43. package/dist/editor/useDocuments.d.ts +4 -1
  44. package/dist/editor/useDocuments.d.ts.map +1 -1
  45. package/dist/editor/useDocuments.js +5 -2
  46. package/dist/editor/useDocuments.js.map +1 -1
  47. package/dist/editor/useP1Editor.d.ts +12 -0
  48. package/dist/editor/useP1Editor.d.ts.map +1 -1
  49. package/dist/editor/useP1Editor.js +40 -5
  50. package/dist/editor/useP1Editor.js.map +1 -1
  51. package/dist/editor/useP1Plugin.d.ts +1 -1
  52. package/dist/editor/useP1Plugin.d.ts.map +1 -1
  53. package/dist/editor/useP1Plugin.js +2 -0
  54. package/dist/editor/useP1Plugin.js.map +1 -1
  55. package/dist/editor/useRealtime.d.ts +9 -4
  56. package/dist/editor/useRealtime.d.ts.map +1 -1
  57. package/dist/editor/useRealtime.js +11 -6
  58. package/dist/editor/useRealtime.js.map +1 -1
  59. package/dist/features/content-type-templates/editor/useTemplateEditor.d.ts +20 -0
  60. package/dist/features/content-type-templates/editor/useTemplateEditor.d.ts.map +1 -0
  61. package/dist/features/content-type-templates/editor/useTemplateEditor.js +62 -0
  62. package/dist/features/content-type-templates/editor/useTemplateEditor.js.map +1 -0
  63. package/dist/features/content-type-templates/editor/useTemplatePermissions.d.ts +7 -0
  64. package/dist/features/content-type-templates/editor/useTemplatePermissions.d.ts.map +1 -0
  65. package/dist/features/content-type-templates/editor/useTemplatePermissions.js +12 -0
  66. package/dist/features/content-type-templates/editor/useTemplatePermissions.js.map +1 -0
  67. package/dist/features/content-type-templates/editor/useTemplateScaffold.d.ts +19 -0
  68. package/dist/features/content-type-templates/editor/useTemplateScaffold.d.ts.map +1 -0
  69. package/dist/features/content-type-templates/editor/useTemplateScaffold.js +37 -0
  70. package/dist/features/content-type-templates/editor/useTemplateScaffold.js.map +1 -0
  71. package/dist/features/content-type-templates/hooks/useTemplateList.d.ts +41 -0
  72. package/dist/features/content-type-templates/hooks/useTemplateList.d.ts.map +1 -0
  73. package/dist/features/content-type-templates/hooks/useTemplateList.js +72 -0
  74. package/dist/features/content-type-templates/hooks/useTemplateList.js.map +1 -0
  75. package/dist/features/content-type-templates/index.d.ts +18 -0
  76. package/dist/features/content-type-templates/index.d.ts.map +1 -0
  77. package/dist/features/content-type-templates/index.js +13 -0
  78. package/dist/features/content-type-templates/index.js.map +1 -0
  79. package/dist/features/content-type-templates/permissions/createPuckPermissions.d.ts +68 -0
  80. package/dist/features/content-type-templates/permissions/createPuckPermissions.d.ts.map +1 -0
  81. package/dist/features/content-type-templates/permissions/createPuckPermissions.js +90 -0
  82. package/dist/features/content-type-templates/permissions/createPuckPermissions.js.map +1 -0
  83. package/dist/features/content-type-templates/permissions/role-permissions.d.ts +49 -0
  84. package/dist/features/content-type-templates/permissions/role-permissions.d.ts.map +1 -0
  85. package/dist/features/content-type-templates/permissions/role-permissions.js +79 -0
  86. package/dist/features/content-type-templates/permissions/role-permissions.js.map +1 -0
  87. package/dist/features/content-type-templates/permissions/useContentRole.d.ts +37 -0
  88. package/dist/features/content-type-templates/permissions/useContentRole.d.ts.map +1 -0
  89. package/dist/features/content-type-templates/permissions/useContentRole.js +34 -0
  90. package/dist/features/content-type-templates/permissions/useContentRole.js.map +1 -0
  91. package/dist/features/content-type-templates/permissions/useResolveContentRole.d.ts +38 -0
  92. package/dist/features/content-type-templates/permissions/useResolveContentRole.d.ts.map +1 -0
  93. package/dist/features/content-type-templates/permissions/useResolveContentRole.js +83 -0
  94. package/dist/features/content-type-templates/permissions/useResolveContentRole.js.map +1 -0
  95. package/dist/features/content-type-templates/stores/index.d.ts +9 -0
  96. package/dist/features/content-type-templates/stores/index.d.ts.map +1 -0
  97. package/dist/features/content-type-templates/stores/index.js +8 -0
  98. package/dist/features/content-type-templates/stores/index.js.map +1 -0
  99. package/dist/features/content-type-templates/stores/template-store-api.d.ts +17 -0
  100. package/dist/features/content-type-templates/stores/template-store-api.d.ts.map +1 -0
  101. package/dist/features/content-type-templates/stores/template-store-api.js +56 -0
  102. package/dist/features/content-type-templates/stores/template-store-api.js.map +1 -0
  103. package/dist/features/content-type-templates/stores/template-store.d.ts +54 -0
  104. package/dist/features/content-type-templates/stores/template-store.d.ts.map +1 -0
  105. package/dist/features/content-type-templates/stores/template-store.js +80 -0
  106. package/dist/features/content-type-templates/stores/template-store.js.map +1 -0
  107. package/dist/features/content-type-templates/types.d.ts +114 -0
  108. package/dist/features/content-type-templates/types.d.ts.map +1 -0
  109. package/dist/features/content-type-templates/types.js +7 -0
  110. package/dist/features/content-type-templates/types.js.map +1 -0
  111. package/dist/features/content-type-templates/ui/ActionBarPinButton.d.ts +3 -0
  112. package/dist/features/content-type-templates/ui/ActionBarPinButton.d.ts.map +1 -0
  113. package/dist/features/content-type-templates/ui/ActionBarPinButton.js +77 -0
  114. package/dist/features/content-type-templates/ui/ActionBarPinButton.js.map +1 -0
  115. package/dist/features/content-type-templates/ui/TemplateManagerOverlay.d.ts +18 -0
  116. package/dist/features/content-type-templates/ui/TemplateManagerOverlay.d.ts.map +1 -0
  117. package/dist/features/content-type-templates/ui/TemplateManagerOverlay.js +167 -0
  118. package/dist/features/content-type-templates/ui/TemplateManagerOverlay.js.map +1 -0
  119. package/dist/features/content-type-templates/ui/TemplatePinPanel.d.ts +18 -0
  120. package/dist/features/content-type-templates/ui/TemplatePinPanel.d.ts.map +1 -0
  121. package/dist/features/content-type-templates/ui/TemplatePinPanel.js +11 -0
  122. package/dist/features/content-type-templates/ui/TemplatePinPanel.js.map +1 -0
  123. package/dist/features/content-type-templates/ui/TemplateSelector.d.ts +41 -0
  124. package/dist/features/content-type-templates/ui/TemplateSelector.d.ts.map +1 -0
  125. package/dist/features/content-type-templates/ui/TemplateSelector.js +32 -0
  126. package/dist/features/content-type-templates/ui/TemplateSelector.js.map +1 -0
  127. package/dist/features/content-type-templates/ui/dataToTemplate.d.ts +25 -0
  128. package/dist/features/content-type-templates/ui/dataToTemplate.d.ts.map +1 -0
  129. package/dist/features/content-type-templates/ui/dataToTemplate.js +37 -0
  130. package/dist/features/content-type-templates/ui/dataToTemplate.js.map +1 -0
  131. package/dist/features/content-type-templates/validation/structural-validation.d.ts +29 -0
  132. package/dist/features/content-type-templates/validation/structural-validation.d.ts.map +1 -0
  133. package/dist/features/content-type-templates/validation/structural-validation.js +69 -0
  134. package/dist/features/content-type-templates/validation/structural-validation.js.map +1 -0
  135. package/dist/index.d.ts +8 -1
  136. package/dist/index.d.ts.map +1 -1
  137. package/dist/index.js +6 -1
  138. package/dist/index.js.map +1 -1
  139. package/dist/p1/editor/connect/connect-field-modal.d.ts.map +1 -1
  140. package/dist/p1/editor/connect/connect-field-modal.js +23 -1
  141. package/dist/p1/editor/connect/connect-field-modal.js.map +1 -1
  142. package/dist/p1/editor/connect/field-connect-plugin.d.ts.map +1 -1
  143. package/dist/p1/editor/connect/field-connect-plugin.js +2 -6
  144. package/dist/p1/editor/connect/field-connect-plugin.js.map +1 -1
  145. package/dist/p1/pages/conflict-resolution-panel.d.ts +17 -0
  146. package/dist/p1/pages/conflict-resolution-panel.d.ts.map +1 -0
  147. package/dist/p1/pages/conflict-resolution-panel.js +74 -0
  148. package/dist/p1/pages/conflict-resolution-panel.js.map +1 -0
  149. package/dist/p1/pages/content-type-template-list.d.ts +7 -0
  150. package/dist/p1/pages/content-type-template-list.d.ts.map +1 -0
  151. package/dist/p1/pages/content-type-template-list.js +108 -0
  152. package/dist/p1/pages/content-type-template-list.js.map +1 -0
  153. package/dist/p1/pages/create-content-type-form.d.ts +7 -0
  154. package/dist/p1/pages/create-content-type-form.d.ts.map +1 -0
  155. package/dist/p1/pages/create-content-type-form.js +102 -0
  156. package/dist/p1/pages/create-content-type-form.js.map +1 -0
  157. package/dist/p1/pages/create-page-form.d.ts +8 -1
  158. package/dist/p1/pages/create-page-form.d.ts.map +1 -1
  159. package/dist/p1/pages/create-page-form.js +67 -8
  160. package/dist/p1/pages/create-page-form.js.map +1 -1
  161. package/dist/p1/pages/create-template-form.d.ts +6 -1
  162. package/dist/p1/pages/create-template-form.d.ts.map +1 -1
  163. package/dist/p1/pages/create-template-form.js +61 -15
  164. package/dist/p1/pages/create-template-form.js.map +1 -1
  165. package/dist/p1/pages/hooks.d.ts +6 -1
  166. package/dist/p1/pages/hooks.d.ts.map +1 -1
  167. package/dist/p1/pages/hooks.js +13 -2
  168. package/dist/p1/pages/hooks.js.map +1 -1
  169. package/dist/p1/pages/index.d.ts +5 -0
  170. package/dist/p1/pages/index.d.ts.map +1 -1
  171. package/dist/p1/pages/index.js +5 -0
  172. package/dist/p1/pages/index.js.map +1 -1
  173. package/dist/p1/pages/migration-conflict-queue.d.ts +8 -0
  174. package/dist/p1/pages/migration-conflict-queue.d.ts.map +1 -0
  175. package/dist/p1/pages/migration-conflict-queue.js +62 -0
  176. package/dist/p1/pages/migration-conflict-queue.js.map +1 -0
  177. package/dist/p1/pages/migration-preview-panel.d.ts +12 -0
  178. package/dist/p1/pages/migration-preview-panel.d.ts.map +1 -0
  179. package/dist/p1/pages/migration-preview-panel.js +157 -0
  180. package/dist/p1/pages/migration-preview-panel.js.map +1 -0
  181. package/dist/p1/pages/migration-status-panel.d.ts +7 -0
  182. package/dist/p1/pages/migration-status-panel.d.ts.map +1 -0
  183. package/dist/p1/pages/migration-status-panel.js +198 -0
  184. package/dist/p1/pages/migration-status-panel.js.map +1 -0
  185. package/dist/p1/pages/structure-page.d.ts.map +1 -1
  186. package/dist/p1/pages/structure-page.js +75 -18
  187. package/dist/p1/pages/structure-page.js.map +1 -1
  188. package/dist/p1/pages/structure-tabs.d.ts +11 -0
  189. package/dist/p1/pages/structure-tabs.d.ts.map +1 -0
  190. package/dist/p1/pages/structure-tabs.js +45 -0
  191. package/dist/p1/pages/structure-tabs.js.map +1 -0
  192. package/dist/pds/components/P1EditorHeader.d.ts +6 -2
  193. package/dist/pds/components/P1EditorHeader.d.ts.map +1 -1
  194. package/dist/pds/components/P1EditorHeader.js +6 -3
  195. package/dist/pds/components/P1EditorHeader.js.map +1 -1
  196. package/dist/pds/components/PageNavigator.d.ts +7 -2
  197. package/dist/pds/components/PageNavigator.d.ts.map +1 -1
  198. package/dist/pds/components/PageNavigator.js +28 -7
  199. package/dist/pds/components/PageNavigator.js.map +1 -1
  200. package/dist/pds/components/PageNavigator.module.css +70 -0
  201. package/package.json +3 -3
  202. package/src/styles.css +331 -0
@@ -15,6 +15,8 @@ import { useDocuments } from './useDocuments.js';
15
15
  import { resolveFeatureConfig } from '../core/featureConfig.js';
16
16
  import { resolveActivePlugins, composeProviders } from './composePlugins.js';
17
17
  import { DEFAULT_CSS_FEATURE_PLUGINS } from './defaultPlugins.js';
18
+ import { createPuckPermissions } from '../features/content-type-templates/permissions/createPuckPermissions.js';
19
+ import { useTemplateList } from '../features/content-type-templates/hooks/useTemplateList.js';
18
20
  /**
19
21
  * Provider component for P1 Puck integration.
20
22
  *
@@ -63,7 +65,10 @@ agentModeEnabled = false, agentId, agentTrigger: _agentTrigger,
63
65
  // Phase 9: Callbacks
64
66
  onPresenceChange, onAgentConflict: _onAgentConflict,
65
67
  // Plugin system props (B.4)
66
- featurePlugins, featureConfig, children, }) {
68
+ featurePlugins, featureConfig,
69
+ // Content Type Templates (PROPOSAL-010)
70
+ // Consumers should resolve the role via useResolveContentRole and pass it here.
71
+ userRole = 'editor', children, }) {
67
72
  // Access notification context
68
73
  const notificationContext = useNotifications();
69
74
  // Persist selected branch in sessionStorage so it survives provider remounts
@@ -120,6 +125,8 @@ featurePlugins, featureConfig, children, }) {
120
125
  // Document state
121
126
  const [currentDocument, setCurrentDocument] = useState(null);
122
127
  const [currentData, setCurrentData] = useState(null);
128
+ // Template state (PROPOSAL-010)
129
+ const [currentTemplate, setCurrentTemplate] = useState(null);
123
130
  // Save state
124
131
  const [saveStatus, setSaveStatus] = useState('idle');
125
132
  const [lastSaved, setLastSaved] = useState(null);
@@ -129,9 +136,10 @@ featurePlugins, featureConfig, children, }) {
129
136
  const currentDocumentRef = useRef(null);
130
137
  const initializedRef = useRef(false);
131
138
  // Track the latest Puck action metadata for inclusion in sync payloads.
132
- // Set by handleAction (Puck's onAction callback), consumed by saveData
133
- // when sending changes via realtime, then cleared.
134
- const lastActionRef = useRef(null);
139
+ // Buffered actions captured by handleAction (Puck's onAction callback)
140
+ // Array accumulates all structural actions during an edit session.
141
+ // Forwarded to backend on save for version history, then cleared.
142
+ const pendingActionsRef = useRef([]);
135
143
  // Tracks the document path that the current data in state belongs to.
136
144
  // Set alongside every setCurrentData call to record the data's origin.
137
145
  // Checked before every write (realtime or REST) to prevent cross-document
@@ -208,6 +216,9 @@ featurePlugins, featureConfig, children, }) {
208
216
  actorType: 'user',
209
217
  enabled: enableRealtime && !!wsBaseUrl,
210
218
  initialData: currentData,
219
+ onServerReload: () => {
220
+ notificationContext.addInfo('Document is being refreshed by the server. Reconnecting...');
221
+ },
211
222
  // WebSocket presence callbacks - receive instant presence updates
212
223
  onPresenceUpdate: (actors) => {
213
224
  // Mark WebSocket presence as active (first update received)
@@ -222,13 +233,7 @@ featurePlugins, featureConfig, children, }) {
222
233
  setWsPresenceActors((prev) => prev.map((a) => (a.actorId === actorId ? { ...a, focusRegions } : a)));
223
234
  },
224
235
  onRemoteUpdate: (data) => {
225
- const componentCount = data.content?.length ?? 0;
226
- const zoneCount = data.zones ? Object.keys(data.zones).length : 0;
227
- console.log('[P1PuckProvider] onRemoteUpdate received:', `components=${componentCount}, zones=${zoneCount},`, `pendingRemoteUpdates=${pendingRemoteUpdatesRef.current},`, `viewingVersion=${viewingVersionRef.current !== null}`);
228
- // Don't apply remote updates while viewing a historical version
229
- // The user is viewing read-only historical data and shouldn't see live changes
230
236
  if (viewingVersionRef.current !== null) {
231
- console.log('[P1PuckProvider] onRemoteUpdate SKIPPED: viewing historical version');
232
237
  return;
233
238
  }
234
239
  // Reject empty data that would overwrite real editor content.
@@ -239,7 +244,6 @@ featurePlugins, featureConfig, children, }) {
239
244
  if (data.content.length === 0 &&
240
245
  (!rootProps || Object.keys(rootProps).length === 0) &&
241
246
  !data.zones) {
242
- console.log('[P1PuckProvider] onRemoteUpdate SKIPPED: empty data rejected');
243
247
  return;
244
248
  }
245
249
  // Store the latest data - will be used when debounce fires
@@ -260,8 +264,6 @@ featurePlugins, featureConfig, children, }) {
260
264
  }
261
265
  const dataToSync = pendingRemoteDataRef.current;
262
266
  if (dataToSync) {
263
- const syncComponentCount = dataToSync.content?.length ?? 0;
264
- console.log('[P1PuckProvider] onRemoteUpdate APPLYING:', `components=${syncComponentCount},`, `pendingRemoteUpdates will be=${pendingRemoteUpdatesRef.current + 1}`);
265
267
  // Increment counter to skip the onChange echo(es) that will fire
266
268
  // when Puck processes the setCurrentData call below.
267
269
  // The content-based guards in puckDataToYMap and RealtimeClient
@@ -342,7 +344,6 @@ featurePlugins, featureConfig, children, }) {
342
344
  const dataOriginPath = currentDataDocumentPathRef.current;
343
345
  if (dataOriginPath !== currentPath)
344
346
  return;
345
- console.log('[P1PuckProvider] PuckDataCapture catch-up: sending missed data,', `components=${currentData.content?.length ?? 0}`);
346
347
  realtimeRef.current.applyLocalChange(currentData);
347
348
  lastSentDataRef.current = dataJson;
348
349
  }, 800);
@@ -369,17 +370,31 @@ featurePlugins, featureConfig, children, }) {
369
370
  const userClient = useMemo(() => client.withPrincipal({ id: userId, type: 'user' }), [client, userId]);
370
371
  // Document list for current branch
371
372
  const { documents: branchDocuments, loading: documentsLoading, create: createDocumentRaw, remove: removeDocumentRaw, } = useDocuments({ client: userClient, siteId, branchId });
373
+ // Template list for current branch
374
+ const { templates: branchTemplates, loading: templatesLoading, error: templatesError, refresh: refreshTemplates, } = useTemplateList(userClient, siteId, branchId);
372
375
  // Stable document create/delete callbacks
373
376
  const branchIdRef = useRef(branchId);
374
377
  branchIdRef.current = branchId;
375
378
  const createDocumentRawRef = useRef(createDocumentRaw);
376
379
  createDocumentRawRef.current = createDocumentRaw;
377
- const stableCreateDocument = useCallback(async (path) => {
380
+ const stableCreateDocument = useCallback(async (path, template) => {
378
381
  if (!branchIdRef.current) {
379
382
  throw new Error('Cannot create document: no branch selected');
380
383
  }
381
- await createDocumentRawRef.current(path);
382
- }, []);
384
+ if (template) {
385
+ // Fetch the full template — the list endpoint may omit the components array
386
+ const fullTemplate = await userClient.templates.get(siteId, branchIdRef.current, template.id);
387
+ const { scaffoldFromTemplate } = await import('../features/content-type-templates/editor/useTemplateScaffold.js');
388
+ const initialData = scaffoldFromTemplate(fullTemplate);
389
+ await createDocumentRawRef.current(path, initialData, {
390
+ templateId: fullTemplate.id,
391
+ templateVersion: fullTemplate.version,
392
+ });
393
+ }
394
+ else {
395
+ await createDocumentRawRef.current(path);
396
+ }
397
+ }, [userClient, siteId]);
383
398
  const removeDocumentRawRef = useRef(removeDocumentRaw);
384
399
  removeDocumentRawRef.current = removeDocumentRaw;
385
400
  const stableDeleteDocument = useCallback(async (documentId, _path) => {
@@ -394,33 +409,29 @@ featurePlugins, featureConfig, children, }) {
394
409
  setBranchesLoading(true);
395
410
  const branchList = await userClient.branches.list(siteId);
396
411
  setBranches(branchList);
397
- // Update current branch from current branchId state
398
- setBranchId((currentBranchId) => {
399
- let effectiveBranchId = currentBranchId;
400
- // If no branchId set, try persisted branch, then default to main
401
- if (!effectiveBranchId) {
402
- const persisted = getPersistedBranchId();
403
- if (persisted && branchList.some((b) => b.id === persisted)) {
404
- effectiveBranchId = persisted;
405
- }
412
+ let effectiveBranchId = branchIdRef.current;
413
+ // If no branchId set, try persisted branch, then default to main
414
+ if (!effectiveBranchId) {
415
+ const persisted = getPersistedBranchId();
416
+ if (persisted && branchList.some((b) => b.id === persisted)) {
417
+ effectiveBranchId = persisted;
406
418
  }
407
- // Validate that the branch exists in the list; fall back to main if not
408
- if (effectiveBranchId && !branchList.some((b) => b.id === effectiveBranchId)) {
409
- const mainBranch = branchList.find((b) => b.isMain);
410
- effectiveBranchId = mainBranch?.id ?? effectiveBranchId;
411
- }
412
- // Default to main if still empty
413
- if (!effectiveBranchId) {
414
- const mainBranch = branchList.find((b) => b.isMain);
415
- if (mainBranch) {
416
- effectiveBranchId = mainBranch.id;
417
- }
419
+ }
420
+ // Validate that the branch exists in the list; fall back to main if not
421
+ if (effectiveBranchId && !branchList.some((b) => b.id === effectiveBranchId)) {
422
+ const mainBranch = branchList.find((b) => b.isMain);
423
+ effectiveBranchId = mainBranch?.id ?? effectiveBranchId;
424
+ }
425
+ // Default to main if still empty
426
+ if (!effectiveBranchId) {
427
+ const mainBranch = branchList.find((b) => b.isMain);
428
+ if (mainBranch) {
429
+ effectiveBranchId = mainBranch.id;
418
430
  }
419
- persistBranchId(effectiveBranchId);
420
- const current = branchList.find((b) => b.id === effectiveBranchId);
421
- setCurrentBranch(current ?? null);
422
- return effectiveBranchId;
423
- });
431
+ }
432
+ persistBranchId(effectiveBranchId);
433
+ setBranchId(effectiveBranchId);
434
+ setCurrentBranch(branchList.find((b) => b.id === effectiveBranchId) ?? null);
424
435
  }
425
436
  catch (error) {
426
437
  console.error('Failed to load branches:', error);
@@ -468,11 +479,16 @@ featurePlugins, featureConfig, children, }) {
468
479
  setSaveError(null);
469
480
  try {
470
481
  await withRetry(async () => {
482
+ const actionsToSend = pendingActionsRef.current.length > 0
483
+ ? [...pendingActionsRef.current]
484
+ : undefined;
471
485
  await userClient.versions.create(siteId, {
472
486
  documentId: doc.id,
473
487
  branchId,
474
488
  snapshot: dataToSave,
489
+ puckActions: actionsToSend,
475
490
  });
491
+ pendingActionsRef.current = [];
476
492
  }, { maxAttempts: maxRetries });
477
493
  // Note: We intentionally do NOT call setCurrentData(dataToSave) here.
478
494
  // The data is already in Puck's internal state (it came from Puck's onChange).
@@ -517,30 +533,34 @@ featurePlugins, featureConfig, children, }) {
517
533
  // This captures the action type and relevant metadata fields so they
518
534
  // can be included in the sync payload for backend version storage.
519
535
  const handleAction = useCallback((action) => {
520
- const actionMetadata = {};
536
+ const puckAction = {
537
+ type: action.type || 'unknown',
538
+ };
521
539
  if (action.componentType)
522
- actionMetadata.componentType = action.componentType;
540
+ puckAction.componentType = action.componentType;
523
541
  if (action.componentId)
524
- actionMetadata.componentId = action.componentId;
542
+ puckAction.componentId = action.componentId;
525
543
  if (action.zone)
526
- actionMetadata.zone = action.zone;
544
+ puckAction.zone = action.zone;
527
545
  if (action.sourceIndex !== undefined)
528
- actionMetadata.sourceIndex = action.sourceIndex;
546
+ puckAction.sourceIndex = action.sourceIndex;
529
547
  if (action.destinationIndex !== undefined)
530
- actionMetadata.destinationIndex = action.destinationIndex;
531
- lastActionRef.current = {
532
- actionType: action.type || 'unknown',
533
- actionMetadata,
534
- };
548
+ puckAction.destinationIndex = action.destinationIndex;
549
+ if (action.sourceZone)
550
+ puckAction.sourceZone = action.sourceZone;
551
+ if (action.destinationZone)
552
+ puckAction.destinationZone = action.destinationZone;
553
+ const buffer = pendingActionsRef.current;
554
+ buffer.push(puckAction);
555
+ if (buffer.length > 1000) {
556
+ pendingActionsRef.current = buffer.slice(-500);
557
+ }
535
558
  }, []);
536
559
  // Public save function (triggers debounce)
537
560
  // Also resumes auto-save if paused, per user requirement
538
561
  // Sends changes via WebSocket when realtime is enabled (but not for remote updates)
539
562
  const saveData = useCallback((data) => {
540
- const componentCount = data.content?.length ?? 0;
541
- // Suppress the onChange echo from PuckDataSynchronizer after loadDocument.
542
563
  if (suppressNextSaveRef.current) {
543
- console.log(`[P1PuckProvider] saveData SKIPPED: suppressNextSave (components=${componentCount})`);
544
564
  suppressNextSaveRef.current = false;
545
565
  return;
546
566
  }
@@ -551,14 +571,10 @@ featurePlugins, featureConfig, children, }) {
551
571
  // getHasUnsavedChanges() will incorrectly report unsaved changes.
552
572
  if (enableRealtime && realtime.connected) {
553
573
  if (pendingRemoteUpdatesRef.current > 0) {
554
- // Counter indicates this onChange is from a remote update or data load
555
- console.log(`[P1PuckProvider] saveData SKIPPED: pendingRemoteUpdates=${pendingRemoteUpdatesRef.current} (components=${componentCount})`);
556
574
  pendingRemoteUpdatesRef.current -= 1;
557
575
  return;
558
576
  }
559
577
  else if (viewingVersionRef.current !== null) {
560
- // User is viewing historical version - don't broadcast or save
561
- console.log(`[P1PuckProvider] saveData SKIPPED: viewing historical version (components=${componentCount})`);
562
578
  return;
563
579
  }
564
580
  else {
@@ -569,24 +585,29 @@ featurePlugins, featureConfig, children, }) {
569
585
  // into state after currentDocument has been updated to X.
570
586
  const dataOriginPath = currentDataDocumentPathRef.current;
571
587
  if (dataOriginPath !== currentPath) {
572
- console.warn('[P1PuckProvider] saveData SKIPPED: data origin mismatch.', 'dataOrigin:', dataOriginPath, 'currentDoc:', currentPath, `components=${componentCount}`);
588
+ console.warn('[P1PuckProvider] saveData SKIPPED: data origin mismatch.', 'dataOrigin:', dataOriginPath, 'currentDoc:', currentPath);
573
589
  return;
574
590
  }
575
591
  // Connection identity guard: verify the realtime connection is
576
592
  // bound to the same document we're currently editing.
577
593
  const connectedPath = realtime.connectedDocumentPath;
578
594
  if (currentPath !== connectedPath) {
579
- console.warn('[P1PuckProvider] saveData SKIPPED: connection identity mismatch.', 'currentDoc:', currentPath, 'connectedDoc:', connectedPath, `components=${componentCount}`);
595
+ console.warn('[P1PuckProvider] saveData SKIPPED: connection identity mismatch.', 'currentDoc:', currentPath, 'connectedDoc:', connectedPath);
580
596
  return;
581
597
  }
582
598
  // Local user edit — send via WebSocket (DO handles persistence)
583
599
  // Echo prevention is handled at lower layers:
584
600
  // - puckDataToYMap no-ops when Y.Doc already has identical data
585
601
  // - RealtimeClient.lastSentSnapshot drops sends matching last sent/received
586
- console.log(`[P1PuckProvider] saveData SENDING via realtime: components=${componentCount}, path=${currentPath}`);
587
- realtime.applyLocalChange(data);
602
+ const actions = pendingActionsRef.current;
603
+ if (actions.length > 0) {
604
+ realtime.applyLocalChange(data, actions);
605
+ }
606
+ else {
607
+ realtime.applyLocalChange(data);
608
+ }
588
609
  trackSentData(data);
589
- lastActionRef.current = null;
610
+ pendingActionsRef.current = [];
590
611
  // Data sent via WebSocket — DO handles persistence.
591
612
  // Update save status directly (skip debouncedSave/performSave chain).
592
613
  setSaveStatus('saved');
@@ -594,8 +615,6 @@ featurePlugins, featureConfig, children, }) {
594
615
  return;
595
616
  }
596
617
  }
597
- // Non-realtime path: mark data as pending and trigger debounced REST save
598
- console.log(`[P1PuckProvider] saveData via REST (debounced): components=${componentCount}, realtimeEnabled=${enableRealtime}, connected=${realtime.connected}`);
599
618
  pendingDataRef.current = data;
600
619
  if (debouncedSave.isPaused()) {
601
620
  debouncedSave.resume();
@@ -658,6 +677,23 @@ featurePlugins, featureConfig, children, }) {
658
677
  currentDataDocumentPathRef.current = null;
659
678
  setCurrentData(null);
660
679
  setCurrentDocument(doc);
680
+ // Fetch template if document is bound to one (PROPOSAL-010)
681
+ if (doc.templateId) {
682
+ try {
683
+ if (!userClient.templates) {
684
+ throw new Error('Templates endpoint not available on client');
685
+ }
686
+ const template = await userClient.templates.get(siteId, branchId, doc.templateId);
687
+ setCurrentTemplate(template);
688
+ }
689
+ catch (templateError) {
690
+ console.warn('[P1PuckProvider] Failed to fetch template:', templateError);
691
+ setCurrentTemplate(null);
692
+ }
693
+ }
694
+ else {
695
+ setCurrentTemplate(null);
696
+ }
661
697
  // Get latest version
662
698
  const version = await userClient.versions.getLatest(siteId, branchId, doc.id);
663
699
  const puckData = version.snapshot;
@@ -805,6 +841,56 @@ featurePlugins, featureConfig, children, }) {
805
841
  // Computed property for whether viewing historical version
806
842
  const isViewingHistoricalVersion = viewingVersion !== null;
807
843
  // =========================================================================
844
+ // Content Type Templates: Permission Resolution (PROPOSAL-010)
845
+ // =========================================================================
846
+ // Resolve template: either from document binding or registry path
847
+ const resolvedTemplate = useMemo(() => {
848
+ if (currentTemplate)
849
+ return currentTemplate;
850
+ const path = currentDocument?.path;
851
+ if (!path)
852
+ return null;
853
+ const match = path.match(/^_registry\/templates\/(.+)$/);
854
+ if (!match)
855
+ return null;
856
+ return branchTemplates.find((t) => t.name === match[1]) ?? null;
857
+ }, [currentTemplate, currentDocument?.path, branchTemplates]);
858
+ // Create Puck permissions resolver based on current template and user role
859
+ const resolvePermissions = useMemo(() => createPuckPermissions(resolvedTemplate, userRole, isViewingHistoricalVersion), [resolvedTemplate, userRole, isViewingHistoricalVersion]);
860
+ // Show notification when template list fails to load
861
+ useEffect(() => {
862
+ if (templatesError && showErrorNotifications && notificationContext?.addError) {
863
+ notificationContext.addError(`Failed to load templates: ${templatesError.message}`, () => void refreshTemplates());
864
+ }
865
+ }, [templatesError]);
866
+ // =========================================================================
867
+ // Content Type Templates: Re-fetch template on tab focus (CUJ-15a)
868
+ // =========================================================================
869
+ useEffect(() => {
870
+ if (typeof document === 'undefined')
871
+ return;
872
+ const handler = async () => {
873
+ if (document.visibilityState !== 'visible')
874
+ return;
875
+ const doc = currentDocumentRef.current;
876
+ if (!doc?.templateId || !userClient.templates)
877
+ return;
878
+ try {
879
+ const freshTemplate = await userClient.templates.get(siteId, branchId, doc.templateId);
880
+ setCurrentTemplate((prev) => {
881
+ if (prev && prev.version === freshTemplate.version)
882
+ return prev;
883
+ return freshTemplate;
884
+ });
885
+ }
886
+ catch {
887
+ // Silently ignore — stale template is better than no template
888
+ }
889
+ };
890
+ document.addEventListener('visibilitychange', handler);
891
+ return () => document.removeEventListener('visibilitychange', handler);
892
+ }, [siteId, userClient]);
893
+ // =========================================================================
808
894
  // Phase 9: Presence & Agent Mode State
809
895
  // =========================================================================
810
896
  // Presence state (when presenceEnabled)
@@ -1254,6 +1340,10 @@ featurePlugins, featureConfig, children, }) {
1254
1340
  const switchBranchRef = useRef(switchBranch);
1255
1341
  switchBranchRef.current = switchBranch;
1256
1342
  const stableSwitchBranch = useCallback((newBranchId) => switchBranchRef.current(newBranchId), []);
1343
+ // Get pending actions for forwarding to backend
1344
+ const getPendingActions = useCallback(() => {
1345
+ return [...pendingActionsRef.current]; // Return copy to prevent external mutation
1346
+ }, []);
1257
1347
  const loadDocumentRef = useRef(loadDocument);
1258
1348
  loadDocumentRef.current = loadDocument;
1259
1349
  const stableLoadDocument = useCallback((path) => loadDocumentRef.current(path), []);
@@ -1388,6 +1478,8 @@ featurePlugins, featureConfig, children, }) {
1388
1478
  sendFocusRegions: realtime.sendFocusRegions,
1389
1479
  // Puck action metadata capture - pass as onAction to <Puck>
1390
1480
  handleAction,
1481
+ // Get pending actions for backend forwarding (PROPOSAL-010)
1482
+ getPendingActions,
1391
1483
  // Phase 9: Presence & Agent values
1392
1484
  // Use getter to avoid context recreation on every focus-region update.
1393
1485
  // Expose humanPresenceCount/hasActiveHumans/hasActiveAgents directly so every
@@ -1406,6 +1498,14 @@ featurePlugins, featureConfig, children, }) {
1406
1498
  // Internal: realtime data capture for catch-up (sends missed keystrokes)
1407
1499
  _realtimeDataCaptureRef: enableRealtime ? realtimeDataCaptureRef : null,
1408
1500
  _onRealtimeDataCapture: enableRealtime ? handleRealtimeDataCapture : null,
1501
+ // Content Type Templates (PROPOSAL-010)
1502
+ userRole,
1503
+ templates: branchTemplates,
1504
+ templatesLoading,
1505
+ templatesError,
1506
+ refreshTemplates,
1507
+ currentTemplate,
1508
+ resolvePermissions,
1409
1509
  }), [
1410
1510
  userClient,
1411
1511
  notificationContext,
@@ -1451,6 +1551,7 @@ featurePlugins, featureConfig, children, }) {
1451
1551
  remoteSyncKey,
1452
1552
  realtime.sendFocusRegions,
1453
1553
  handleAction,
1554
+ getPendingActions,
1454
1555
  handleRealtimeDataCapture,
1455
1556
  // Phase 9 dependencies (full presenceState excluded — accessed via getter/ref,
1456
1557
  // but humanPresenceCount/hasActiveHumans/hasActiveAgents are direct values so every
@@ -1465,6 +1566,14 @@ featurePlugins, featureConfig, children, }) {
1465
1566
  dismissConflict,
1466
1567
  enableRealtime,
1467
1568
  resolvedFeatureConfig,
1569
+ // Content Type Templates
1570
+ userRole,
1571
+ branchTemplates,
1572
+ templatesLoading,
1573
+ templatesError,
1574
+ refreshTemplates,
1575
+ currentTemplate,
1576
+ resolvePermissions,
1468
1577
  ]);
1469
1578
  // Wrap with PresenceContext.Provider when presence is enabled
1470
1579
  const wrappedChildren = presenceContextValue ? (_jsx(PresenceContext.Provider, { value: presenceContextValue, children: children })) : (children);