@elytracms/studio 0.0.42 → 0.0.44

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 (229) hide show
  1. package/dist/components/app-views/studio-config.d.ts +12 -0
  2. package/dist/components/app-views/studio-config.js.map +1 -1
  3. package/dist/components/assets/asset-detail.js +1 -1
  4. package/dist/components/assets/asset-detail.js.map +1 -1
  5. package/dist/components/assets/assets-collection-view-source.d.ts +13 -1
  6. package/dist/components/assets/assets-collection-view-source.js +42 -51
  7. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  8. package/dist/components/assets/assets-module.js +19 -6
  9. package/dist/components/assets/assets-module.js.map +1 -1
  10. package/dist/components/assets/assets-store.d.ts +2 -1
  11. package/dist/components/assets/assets-store.js +4 -8
  12. package/dist/components/assets/assets-store.js.map +1 -1
  13. package/dist/components/assets/assets-view-preference.d.ts +0 -8
  14. package/dist/components/assets/assets-view-preference.js +12 -13
  15. package/dist/components/assets/assets-view-preference.js.map +1 -1
  16. package/dist/components/auth-context.d.ts +2 -1
  17. package/dist/components/auth-context.js +6 -8
  18. package/dist/components/auth-context.js.map +1 -1
  19. package/dist/components/builder/builder-store.d.ts +2 -2
  20. package/dist/components/builder/builder-store.js +4 -8
  21. package/dist/components/builder/builder-store.js.map +1 -1
  22. package/dist/components/cms/bulk-actions.js +2 -1
  23. package/dist/components/cms/bulk-actions.js.map +1 -1
  24. package/dist/components/cms/cell-edit.js +2 -1
  25. package/dist/components/cms/cell-edit.js.map +1 -1
  26. package/dist/components/cms/cms-composition-validators.d.ts +25 -0
  27. package/dist/components/cms/cms-composition-validators.js +75 -0
  28. package/dist/components/cms/cms-composition-validators.js.map +1 -0
  29. package/dist/components/cms/cms-labels.d.ts +15 -0
  30. package/dist/components/cms/cms-labels.js +27 -0
  31. package/dist/components/cms/cms-labels.js.map +1 -0
  32. package/dist/components/cms/cms-store.d.ts +23 -10
  33. package/dist/components/cms/cms-store.js +87 -165
  34. package/dist/components/cms/cms-store.js.map +1 -1
  35. package/dist/components/cms/collection-table.js +1 -1
  36. package/dist/components/cms/collection-table.js.map +1 -1
  37. package/dist/components/cms/detail-editor.js +111 -348
  38. package/dist/components/cms/detail-editor.js.map +1 -1
  39. package/dist/components/cms/discard-draft.d.ts +18 -0
  40. package/dist/components/cms/discard-draft.js +18 -0
  41. package/dist/components/cms/discard-draft.js.map +1 -1
  42. package/dist/components/cms/document-editor-context.d.ts +2 -1
  43. package/dist/components/cms/document-editor-context.js +4 -9
  44. package/dist/components/cms/document-editor-context.js.map +1 -1
  45. package/dist/components/cms/document-ref-utils.d.ts +33 -0
  46. package/dist/components/cms/document-ref-utils.js +47 -0
  47. package/dist/components/cms/document-ref-utils.js.map +1 -0
  48. package/dist/components/cms/document-staging.d.ts +8 -3
  49. package/dist/components/cms/document-staging.js +81 -21
  50. package/dist/components/cms/document-staging.js.map +1 -1
  51. package/dist/components/cms/field-control.js +1 -1
  52. package/dist/components/cms/field-control.js.map +1 -1
  53. package/dist/components/cms/issues-pane.js +1 -1
  54. package/dist/components/cms/issues-pane.js.map +1 -1
  55. package/dist/components/cms/rich-text/block-commands.d.ts +36 -0
  56. package/dist/components/cms/rich-text/block-commands.js +47 -0
  57. package/dist/components/cms/rich-text/block-commands.js.map +1 -0
  58. package/dist/components/cms/rich-text/slash-command.js +25 -8
  59. package/dist/components/cms/rich-text/slash-command.js.map +1 -1
  60. package/dist/components/cms/rich-text/toolbar.js +3 -2
  61. package/dist/components/cms/rich-text/toolbar.js.map +1 -1
  62. package/dist/components/cms/use-compare-selection.d.ts +27 -0
  63. package/dist/components/cms/use-compare-selection.js +29 -0
  64. package/dist/components/cms/use-compare-selection.js.map +1 -0
  65. package/dist/components/cms/use-discard-controller.d.ts +44 -0
  66. package/dist/components/cms/use-discard-controller.js +51 -0
  67. package/dist/components/cms/use-discard-controller.js.map +1 -0
  68. package/dist/components/cms/use-embed-inspector-host.d.ts +12 -0
  69. package/dist/components/cms/use-embed-inspector-host.js +17 -0
  70. package/dist/components/cms/use-embed-inspector-host.js.map +1 -0
  71. package/dist/components/cms/use-field-focus.d.ts +38 -0
  72. package/dist/components/cms/use-field-focus.js +70 -0
  73. package/dist/components/cms/use-field-focus.js.map +1 -0
  74. package/dist/components/cms/use-live-url.d.ts +41 -0
  75. package/dist/components/cms/use-live-url.js +55 -0
  76. package/dist/components/cms/use-live-url.js.map +1 -0
  77. package/dist/components/cms/use-publish-controller.d.ts +49 -0
  78. package/dist/components/cms/use-publish-controller.js +102 -0
  79. package/dist/components/cms/use-publish-controller.js.map +1 -0
  80. package/dist/components/cms/use-published-perspective.d.ts +67 -0
  81. package/dist/components/cms/use-published-perspective.js +67 -0
  82. package/dist/components/cms/use-published-perspective.js.map +1 -0
  83. package/dist/components/cms/use-restore-version.d.ts +57 -0
  84. package/dist/components/cms/use-restore-version.js +66 -0
  85. package/dist/components/cms/use-restore-version.js.map +1 -0
  86. package/dist/components/cms/use-save-mode.d.ts +35 -0
  87. package/dist/components/cms/use-save-mode.js +54 -0
  88. package/dist/components/cms/use-save-mode.js.map +1 -0
  89. package/dist/components/cms/version-compare.js +14 -17
  90. package/dist/components/cms/version-compare.js.map +1 -1
  91. package/dist/components/cms/versions-model.d.ts +31 -0
  92. package/dist/components/cms/versions-model.js +30 -0
  93. package/dist/components/cms/versions-model.js.map +1 -1
  94. package/dist/components/cms/versions-pane.js +21 -19
  95. package/dist/components/cms/versions-pane.js.map +1 -1
  96. package/dist/components/cms/view-bar.js +9 -6
  97. package/dist/components/cms/view-bar.js.map +1 -1
  98. package/dist/components/collab/collab-context.d.ts +2 -1
  99. package/dist/components/collab/collab-context.js +4 -8
  100. package/dist/components/collab/collab-context.js.map +1 -1
  101. package/dist/components/collection-view/collection-view-context.d.ts +9 -1
  102. package/dist/components/collection-view/collection-view-context.js +4 -9
  103. package/dist/components/collection-view/collection-view-context.js.map +1 -1
  104. package/dist/components/components/component-detail.js +2 -5
  105. package/dist/components/components/component-detail.js.map +1 -1
  106. package/dist/components/components/component-preview.js +8 -4
  107. package/dist/components/components/component-preview.js.map +1 -1
  108. package/dist/components/components/registry-browser.js +24 -5
  109. package/dist/components/components/registry-browser.js.map +1 -1
  110. package/dist/components/components/transfer-dialog.d.ts +21 -6
  111. package/dist/components/components/transfer-dialog.js +120 -31
  112. package/dist/components/components/transfer-dialog.js.map +1 -1
  113. package/dist/components/components/transfer.d.ts +74 -8
  114. package/dist/components/components/transfer.js +87 -2
  115. package/dist/components/components/transfer.js.map +1 -1
  116. package/dist/components/delivery-health-banner.d.ts +12 -6
  117. package/dist/components/delivery-health-banner.js +27 -29
  118. package/dist/components/delivery-health-banner.js.map +1 -1
  119. package/dist/components/dev-login-badge.d.ts +0 -2
  120. package/dist/components/dev-login-badge.js +1 -3
  121. package/dist/components/dev-login-badge.js.map +1 -1
  122. package/dist/components/playground-badge.d.ts +0 -2
  123. package/dist/components/playground-badge.js +0 -2
  124. package/dist/components/playground-badge.js.map +1 -1
  125. package/dist/components/project-list.js +2 -6
  126. package/dist/components/project-list.js.map +1 -1
  127. package/dist/components/project-session.d.ts +6 -2
  128. package/dist/components/project-session.js +32 -83
  129. package/dist/components/project-session.js.map +1 -1
  130. package/dist/components/project-switcher.js +2 -6
  131. package/dist/components/project-switcher.js.map +1 -1
  132. package/dist/components/rail-store-status.d.ts +7 -5
  133. package/dist/components/rail-store-status.js +12 -9
  134. package/dist/components/rail-store-status.js.map +1 -1
  135. package/dist/components/rail-view-source.d.ts +111 -0
  136. package/dist/components/rail-view-source.js +139 -0
  137. package/dist/components/rail-view-source.js.map +1 -0
  138. package/dist/components/redirects/redirects-collection-view-source.d.ts +13 -1
  139. package/dist/components/redirects/redirects-collection-view-source.js +42 -70
  140. package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
  141. package/dist/components/redirects/redirects-store.d.ts +2 -1
  142. package/dist/components/redirects/redirects-store.js +4 -8
  143. package/dist/components/redirects/redirects-store.js.map +1 -1
  144. package/dist/components/section-state.d.ts +12 -3
  145. package/dist/components/section-state.js +8 -4
  146. package/dist/components/section-state.js.map +1 -1
  147. package/dist/components/studio-context.d.ts +2 -1
  148. package/dist/components/studio-context.js +4 -8
  149. package/dist/components/studio-context.js.map +1 -1
  150. package/dist/components/studio-shell.js +1 -1
  151. package/dist/components/studio-shell.js.map +1 -1
  152. package/dist/components/theme-context.d.ts +2 -1
  153. package/dist/components/theme-context.js +4 -8
  154. package/dist/components/theme-context.js.map +1 -1
  155. package/dist/components/use-hydration-phase.d.ts +65 -0
  156. package/dist/components/use-hydration-phase.js +64 -0
  157. package/dist/components/use-hydration-phase.js.map +1 -0
  158. package/dist/components/users/users-collection-view-source.d.ts +13 -1
  159. package/dist/components/users/users-collection-view-source.js +60 -79
  160. package/dist/components/users/users-collection-view-source.js.map +1 -1
  161. package/dist/components/users/users-store.d.ts +2 -1
  162. package/dist/components/users/users-store.js +4 -8
  163. package/dist/components/users/users-store.js.map +1 -1
  164. package/dist/components/workspace-projects.d.ts +14 -0
  165. package/dist/components/workspace-projects.js +21 -0
  166. package/dist/components/workspace-projects.js.map +1 -1
  167. package/dist/lib/ai/anthropic-provider.d.ts +29 -0
  168. package/dist/lib/ai/anthropic-provider.js +59 -0
  169. package/dist/lib/ai/anthropic-provider.js.map +1 -0
  170. package/dist/lib/ai/apply-content.d.ts +9 -0
  171. package/dist/lib/ai/apply-content.js +170 -0
  172. package/dist/lib/ai/apply-content.js.map +1 -0
  173. package/dist/lib/ai/config.d.ts +58 -0
  174. package/dist/lib/ai/config.js +134 -0
  175. package/dist/lib/ai/config.js.map +1 -0
  176. package/dist/lib/ai/fake-provider.d.ts +34 -0
  177. package/dist/lib/ai/fake-provider.js +31 -0
  178. package/dist/lib/ai/fake-provider.js.map +1 -0
  179. package/dist/lib/ai/index.d.ts +22 -0
  180. package/dist/lib/ai/index.js +23 -0
  181. package/dist/lib/ai/index.js.map +1 -0
  182. package/dist/lib/ai/knowledge.d.ts +39 -0
  183. package/dist/lib/ai/knowledge.js +60 -0
  184. package/dist/lib/ai/knowledge.js.map +1 -0
  185. package/dist/lib/ai/provenance.d.ts +20 -0
  186. package/dist/lib/ai/provenance.js +15 -0
  187. package/dist/lib/ai/provenance.js.map +1 -0
  188. package/dist/lib/ai/route.d.ts +24 -0
  189. package/dist/lib/ai/route.js +90 -0
  190. package/dist/lib/ai/route.js.map +1 -0
  191. package/dist/lib/ai/shape.d.ts +35 -0
  192. package/dist/lib/ai/shape.js +113 -0
  193. package/dist/lib/ai/shape.js.map +1 -0
  194. package/dist/lib/ai/types.d.ts +227 -0
  195. package/dist/lib/ai/types.js +2 -0
  196. package/dist/lib/ai/types.js.map +1 -0
  197. package/dist/lib/data/data-store.d.ts +2 -1
  198. package/dist/lib/data/data-store.js +4 -8
  199. package/dist/lib/data/data-store.js.map +1 -1
  200. package/dist/lib/delivery/instant-publish.d.ts +9 -13
  201. package/dist/lib/delivery/instant-publish.js +0 -7
  202. package/dist/lib/delivery/instant-publish.js.map +1 -1
  203. package/dist/lib/diff-view-preference.js +4 -4
  204. package/dist/lib/diff-view-preference.js.map +1 -1
  205. package/dist/lib/i18n/messages.en.js +5 -2
  206. package/dist/lib/i18n/messages.en.js.map +1 -1
  207. package/dist/lib/local-preference.d.ts +37 -0
  208. package/dist/lib/local-preference.js +36 -0
  209. package/dist/lib/local-preference.js.map +1 -1
  210. package/dist/lib/persistence/convex-adapter.d.ts +3 -1
  211. package/dist/lib/persistence/convex-adapter.js +20 -0
  212. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  213. package/dist/lib/rail.js +7 -6
  214. package/dist/lib/rail.js.map +1 -1
  215. package/dist/lib/required-context.d.ts +24 -0
  216. package/dist/lib/required-context.js +28 -0
  217. package/dist/lib/required-context.js.map +1 -0
  218. package/dist/lib/theme.js +11 -4
  219. package/dist/lib/theme.js.map +1 -1
  220. package/dist/lib/workspace/index.d.ts +9 -0
  221. package/dist/lib/workspace/index.js +11 -0
  222. package/dist/lib/workspace/index.js.map +1 -1
  223. package/dist/ui/index.d.ts +1 -0
  224. package/dist/ui/index.js +1 -0
  225. package/dist/ui/index.js.map +1 -1
  226. package/dist/ui/segmented-control.d.ts +46 -0
  227. package/dist/ui/segmented-control.js +13 -0
  228. package/dist/ui/segmented-control.js.map +1 -0
  229. package/package.json +8 -2
@@ -1 +1 @@
1
- {"version":3,"file":"discard-draft.js","sourceRoot":"","sources":["../../../src/components/cms/discard-draft.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAA;AAuBhE,MAAM,UAAU,kBAAkB,CAAC,KAWlC;IACC,IAAI,CAAC,KAAK,CAAC,UAAU,IAAI,CAAC,KAAK,CAAC,SAAS;QAAE,OAAO,QAAQ,CAAA;IAC1D,IAAI,KAAK,CAAC,YAAY;QAAE,OAAO,gBAAgB,CAAA;IAC/C,IAAI,KAAK,CAAC,iBAAiB,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IAC3D,OAAO,sBAAsB,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,iBAAiB,EAAE,KAAK,CAAC,KAAK,CAAC;QACnF,CAAC,CAAC,YAAY;QACd,CAAC,CAAC,OAAO,CAAA;AACb,CAAC","sourcesContent":["import { documentsSemanticEqual } from '@elytracms/core/content'\nimport type { CmsDocument, CollectionDef } from '@elytracms/core/cms-core'\n\n/**\n * EC-316 — the \"Discard changes\" header action's enablement, as one pure\n * decision. Discarding reverts the draft to exactly the published content (via\n * the EC-298 restore machinery), so it is offered iff there is a published\n * version to revert TO and the draft actually differs from it:\n *\n * - `hidden` — the rail can't discard at all (no publish/versions\n * capability) or the document isn't published: there is no\n * published content to revert to, so no menu item.\n * - `view-read-only`— viewing the Published snapshot (EC-226): the draft isn't\n * the active view, so the item is disabled with a hint.\n * - `loading` — the version history hasn't resolved yet; disabled.\n * - `up-to-date` — the draft SEMANTICALLY equals the published snapshot\n * (`documentsSemanticEqual`, the SAME comparator the\n * Versions pane's publish gate uses, EC-298 — so the two\n * surfaces never contradict); nothing to discard, disabled.\n * - `ready` — a published version exists and the draft differs: enabled.\n */\nexport type DiscardDraftStatus = 'hidden' | 'view-read-only' | 'loading' | 'up-to-date' | 'ready'\n\nexport function discardDraftStatus(input: {\n /** The rail can publish AND has version history (both required to discard). */\n canDiscard: boolean\n /** The document IS published (version pointer set). */\n published: boolean\n /** The editor is viewing the read-only Published snapshot (EC-226). */\n viewReadOnly: boolean\n /** The pinned published version's snapshot; `undefined` while loading / none pinned. */\n publishedSnapshot: CmsDocument | undefined\n collection: CollectionDef\n draft: CmsDocument\n}): DiscardDraftStatus {\n if (!input.canDiscard || !input.published) return 'hidden'\n if (input.viewReadOnly) return 'view-read-only'\n if (input.publishedSnapshot === undefined) return 'loading'\n return documentsSemanticEqual(input.collection, input.publishedSnapshot, input.draft)\n ? 'up-to-date'\n : 'ready'\n}\n"]}
1
+ {"version":3,"file":"discard-draft.js","sourceRoot":"","sources":["../../../src/components/cms/discard-draft.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAA;AAuBhE,MAAM,UAAU,kBAAkB,CAAC,KAWlC;IACC,IAAI,CAAC,KAAK,CAAC,UAAU,IAAI,CAAC,KAAK,CAAC,SAAS;QAAE,OAAO,QAAQ,CAAA;IAC1D,IAAI,KAAK,CAAC,YAAY;QAAE,OAAO,gBAAgB,CAAA;IAC/C,IAAI,KAAK,CAAC,iBAAiB,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IAC3D,OAAO,sBAAsB,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,iBAAiB,EAAE,KAAK,CAAC,KAAK,CAAC;QACnF,CAAC,CAAC,YAAY;QACd,CAAC,CAAC,OAAO,CAAA;AACb,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAAC,KAA2C;IACzE,IAAI,KAAK,CAAC,KAAK;QAAE,KAAK,CAAC,IAAI,EAAE,CAAA;AAC/B,CAAC","sourcesContent":["import { documentsSemanticEqual } from '@elytracms/core/content'\nimport type { CmsDocument, CollectionDef } from '@elytracms/core/cms-core'\n\n/**\n * EC-316 — the \"Discard changes\" header action's enablement, as one pure\n * decision. Discarding reverts the draft to exactly the published content (via\n * the EC-298 restore machinery), so it is offered iff there is a published\n * version to revert TO and the draft actually differs from it:\n *\n * - `hidden` — the rail can't discard at all (no publish/versions\n * capability) or the document isn't published: there is no\n * published content to revert to, so no menu item.\n * - `view-read-only`— viewing the Published snapshot (EC-226): the draft isn't\n * the active view, so the item is disabled with a hint.\n * - `loading` — the version history hasn't resolved yet; disabled.\n * - `up-to-date` — the draft SEMANTICALLY equals the published snapshot\n * (`documentsSemanticEqual`, the SAME comparator the\n * Versions pane's publish gate uses, EC-298 — so the two\n * surfaces never contradict); nothing to discard, disabled.\n * - `ready` — a published version exists and the draft differs: enabled.\n */\nexport type DiscardDraftStatus = 'hidden' | 'view-read-only' | 'loading' | 'up-to-date' | 'ready'\n\nexport function discardDraftStatus(input: {\n /** The rail can publish AND has version history (both required to discard). */\n canDiscard: boolean\n /** The document IS published (version pointer set). */\n published: boolean\n /** The editor is viewing the read-only Published snapshot (EC-226). */\n viewReadOnly: boolean\n /** The pinned published version's snapshot; `undefined` while loading / none pinned. */\n publishedSnapshot: CmsDocument | undefined\n collection: CollectionDef\n draft: CmsDocument\n}): DiscardDraftStatus {\n if (!input.canDiscard || !input.published) return 'hidden'\n if (input.viewReadOnly) return 'view-read-only'\n if (input.publishedSnapshot === undefined) return 'loading'\n return documentsSemanticEqual(input.collection, input.publishedSnapshot, input.draft)\n ? 'up-to-date'\n : 'ready'\n}\n\n/**\n * EC-325 — the flush-before-restore ordering \"Discard changes\" must follow,\n * mirroring the IDENTICAL `if (dirty) save()` step the Versions pane's\n * (`versions-pane.tsx`'s `handleRestore`) and the Compare view's\n * (`version-compare.tsx`'s `restore`) own restores already take before theirs.\n * `documents.restore` (`packages/core/src/operations/documents.ts`) snapshots\n * whatever is currently PERSISTED as its own \"pre-restore\" version — never the\n * client's in-memory draft — so an edit still sitting in the autosave debounce\n * window when Discard fires would otherwise never reach the server and be\n * silently unrecoverable once the draft is reverted. The underlying\n * save/restore commands are serialized by the shared operations queue\n * (EC-225), so the un-awaited `save()` here still lands before `restore` reads\n * the persisted document.\n */\nexport function flushDirtyDraft(input: { dirty: boolean; save: () => void }): void {\n if (input.dirty) input.save()\n}\n"]}
@@ -60,11 +60,12 @@ export interface DocumentEditorContextValue {
60
60
  /** Leave compare mode, back to editing. */
61
61
  exitCompare: () => void;
62
62
  }
63
+ declare const useDocumentEditor: () => DocumentEditorContextValue;
63
64
  export declare function DocumentEditorProvider({ value, children, }: {
64
65
  value: DocumentEditorContextValue;
65
66
  children: ReactNode;
66
67
  }): import("react").JSX.Element;
67
- export declare function useDocumentEditor(): DocumentEditorContextValue;
68
+ export { useDocumentEditor };
68
69
  /**
69
70
  * The open document's editing context, or null when there is none (e.g. a
70
71
  * relation picker opened from a table cell rather than the detail editor). Lets a
@@ -1,16 +1,11 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { createContext, useContext } from 'react';
3
- const DocumentEditorContext = createContext(null);
2
+ import { useContext } from 'react';
3
+ import { createRequiredContext } from '@elytracms/studio/lib/required-context';
4
+ const { Context: DocumentEditorContext, useRequired: useDocumentEditor } = createRequiredContext('DocumentEditor');
4
5
  export function DocumentEditorProvider({ value, children, }) {
5
6
  return _jsx(DocumentEditorContext.Provider, { value: value, children: children });
6
7
  }
7
- export function useDocumentEditor() {
8
- const value = useContext(DocumentEditorContext);
9
- if (!value) {
10
- throw new Error('useDocumentEditor must be used within a <DocumentEditorProvider>.');
11
- }
12
- return value;
13
- }
8
+ export { useDocumentEditor };
14
9
  /**
15
10
  * The open document's editing context, or null when there is none (e.g. a
16
11
  * relation picker opened from a table cell rather than the detail editor). Lets a
@@ -1 +1 @@
1
- {"version":3,"file":"document-editor-context.js","sourceRoot":"","sources":["../../../src/components/cms/document-editor-context.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,OAAO,CAAA;AAkEjD,MAAM,qBAAqB,GAAG,aAAa,CAAoC,IAAI,CAAC,CAAA;AAEpF,MAAM,UAAU,sBAAsB,CAAC,EACrC,KAAK,EACL,QAAQ,GAIT;IACC,OAAO,KAAC,qBAAqB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAAkC,CAAA;AAClG,CAAC;AAED,MAAM,UAAU,iBAAiB;IAC/B,MAAM,KAAK,GAAG,UAAU,CAAC,qBAAqB,CAAC,CAAA;IAC/C,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,mEAAmE,CAAC,CAAA;IACtF,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB;IACvC,OAAO,UAAU,CAAC,qBAAqB,CAAC,CAAA;AAC1C,CAAC","sourcesContent":["import { createContext, useContext } from 'react'\nimport type { ReactNode } from 'react'\nimport type { CmsDocument, CollectionDef, DocumentRef } from '@elytracms/core/cms-core'\n\n/**\n * EC-224 — the open document's editing context, provided by the detail editor\n * and consumed by document-level inspector panes (the `Versions` pane).\n *\n * It exists because the Versions pane is rendered in two places — directly in\n * the form view's sidebar, and inside the canvas view's own sidebar (deep in the\n * scoped `BuilderProvider` subtree). A context lets the pane reach the open\n * document's ref + live draft + actions from either mount point, without\n * threading props through the canvas field editor.\n */\n/**\n * EC-298 version-compare selection: which two snapshots the compare view diffs.\n * `base` is a recorded version number (the older side); `target` is either\n * another recorded version or the live working draft.\n */\nexport interface CompareState {\n base: number\n target: number | 'draft'\n}\n\nexport interface DocumentEditorContextValue {\n /** The project being edited (undefined in fixture/playground sessions). */\n projectId: string | undefined\n collection: CollectionDef\n docRef: DocumentRef\n /** The editor's live (possibly unsaved) draft buffer. */\n draft: CmsDocument\n /** Does the draft buffer diverge from the saved document? */\n dirty: boolean\n /** Does the draft have error-severity validation issues (blocks publish)? */\n hasErrors: boolean\n /**\n * Is the editor in the read-only Published view (EC-226)? Document-level panes\n * must disable their mutating actions (publish/restore) while it is true.\n */\n viewReadOnly: boolean\n /** Persist the current draft buffer (the editor's Save). */\n save: () => void\n /**\n * Replace the editor's draft buffer — used after a version restore swaps the\n * working draft for an older snapshot, so the open form/canvas reflect it.\n */\n replaceDraft: (doc: CmsDocument) => void\n /**\n * EC-298 — the open version comparison, or null when none. The Compare tab is a\n * sticky content tab: this selection persists while the user tabs between the\n * diff and the real fields, and is cleared only by closing the tab (its ✕).\n */\n compare: CompareState | null\n /** Is the Compare tab the currently VISIBLE view? Drives the Versions pane's\n * base-picker mode — the pane navigates only while the diff is on screen. */\n compareActive: boolean\n /** Open (or refocus) the Compare tab with `base` vs the current draft. */\n startCompare: (base: number) => void\n /** Change which recorded version is the (older) base side. */\n setCompareBase: (base: number) => void\n /** Change what the base is compared against (a version, or the live draft). */\n setCompareTarget: (target: number | 'draft') => void\n /** Leave compare mode, back to editing. */\n exitCompare: () => void\n}\n\nconst DocumentEditorContext = createContext<DocumentEditorContextValue | null>(null)\n\nexport function DocumentEditorProvider({\n value,\n children,\n}: {\n value: DocumentEditorContextValue\n children: ReactNode\n}) {\n return <DocumentEditorContext.Provider value={value}>{children}</DocumentEditorContext.Provider>\n}\n\nexport function useDocumentEditor(): DocumentEditorContextValue {\n const value = useContext(DocumentEditorContext)\n if (!value) {\n throw new Error('useDocumentEditor must be used within a <DocumentEditorProvider>.')\n }\n return value\n}\n\n/**\n * The open document's editing context, or null when there is none (e.g. a\n * relation picker opened from a table cell rather than the detail editor). Lets a\n * control opt into doc-aware behavior — like the EC-218 parent cycle guard —\n * without requiring the provider.\n */\nexport function useOptionalDocumentEditor(): DocumentEditorContextValue | null {\n return useContext(DocumentEditorContext)\n}\n"]}
1
+ {"version":3,"file":"document-editor-context.js","sourceRoot":"","sources":["../../../src/components/cms/document-editor-context.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,OAAO,CAAA;AAGlC,OAAO,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAA;AAgE9E,MAAM,EAAE,OAAO,EAAE,qBAAqB,EAAE,WAAW,EAAE,iBAAiB,EAAE,GACtE,qBAAqB,CAA6B,gBAAgB,CAAC,CAAA;AAErE,MAAM,UAAU,sBAAsB,CAAC,EACrC,KAAK,EACL,QAAQ,GAIT;IACC,OAAO,KAAC,qBAAqB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAAkC,CAAA;AAClG,CAAC;AAED,OAAO,EAAE,iBAAiB,EAAE,CAAA;AAE5B;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB;IACvC,OAAO,UAAU,CAAC,qBAAqB,CAAC,CAAA;AAC1C,CAAC","sourcesContent":["import { useContext } from 'react'\nimport type { ReactNode } from 'react'\nimport type { CmsDocument, CollectionDef, DocumentRef } from '@elytracms/core/cms-core'\nimport { createRequiredContext } from '@elytracms/studio/lib/required-context'\n\n/**\n * EC-224 — the open document's editing context, provided by the detail editor\n * and consumed by document-level inspector panes (the `Versions` pane).\n *\n * It exists because the Versions pane is rendered in two places — directly in\n * the form view's sidebar, and inside the canvas view's own sidebar (deep in the\n * scoped `BuilderProvider` subtree). A context lets the pane reach the open\n * document's ref + live draft + actions from either mount point, without\n * threading props through the canvas field editor.\n */\n/**\n * EC-298 version-compare selection: which two snapshots the compare view diffs.\n * `base` is a recorded version number (the older side); `target` is either\n * another recorded version or the live working draft.\n */\nexport interface CompareState {\n base: number\n target: number | 'draft'\n}\n\nexport interface DocumentEditorContextValue {\n /** The project being edited (undefined in fixture/playground sessions). */\n projectId: string | undefined\n collection: CollectionDef\n docRef: DocumentRef\n /** The editor's live (possibly unsaved) draft buffer. */\n draft: CmsDocument\n /** Does the draft buffer diverge from the saved document? */\n dirty: boolean\n /** Does the draft have error-severity validation issues (blocks publish)? */\n hasErrors: boolean\n /**\n * Is the editor in the read-only Published view (EC-226)? Document-level panes\n * must disable their mutating actions (publish/restore) while it is true.\n */\n viewReadOnly: boolean\n /** Persist the current draft buffer (the editor's Save). */\n save: () => void\n /**\n * Replace the editor's draft buffer — used after a version restore swaps the\n * working draft for an older snapshot, so the open form/canvas reflect it.\n */\n replaceDraft: (doc: CmsDocument) => void\n /**\n * EC-298 — the open version comparison, or null when none. The Compare tab is a\n * sticky content tab: this selection persists while the user tabs between the\n * diff and the real fields, and is cleared only by closing the tab (its ✕).\n */\n compare: CompareState | null\n /** Is the Compare tab the currently VISIBLE view? Drives the Versions pane's\n * base-picker mode — the pane navigates only while the diff is on screen. */\n compareActive: boolean\n /** Open (or refocus) the Compare tab with `base` vs the current draft. */\n startCompare: (base: number) => void\n /** Change which recorded version is the (older) base side. */\n setCompareBase: (base: number) => void\n /** Change what the base is compared against (a version, or the live draft). */\n setCompareTarget: (target: number | 'draft') => void\n /** Leave compare mode, back to editing. */\n exitCompare: () => void\n}\n\nconst { Context: DocumentEditorContext, useRequired: useDocumentEditor } =\n createRequiredContext<DocumentEditorContextValue>('DocumentEditor')\n\nexport function DocumentEditorProvider({\n value,\n children,\n}: {\n value: DocumentEditorContextValue\n children: ReactNode\n}) {\n return <DocumentEditorContext.Provider value={value}>{children}</DocumentEditorContext.Provider>\n}\n\nexport { useDocumentEditor }\n\n/**\n * The open document's editing context, or null when there is none (e.g. a\n * relation picker opened from a table cell rather than the detail editor). Lets a\n * control opt into doc-aware behavior — like the EC-218 parent cycle guard —\n * without requiring the provider.\n */\nexport function useOptionalDocumentEditor(): DocumentEditorContextValue | null {\n return useContext(DocumentEditorContext)\n}\n"]}
@@ -0,0 +1,33 @@
1
+ import type { CmsDocument, DocumentRef } from '@elytracms/core/cms-core';
2
+ /**
3
+ * By-ref document array helpers (EC-326) — a shared idiom over
4
+ * `@elytracms/core/cms-core`'s `documentRefEquals`, replacing ~12 hand-inlined
5
+ * `d.collection === x.collection && d.id === x.id` predicates across
6
+ * `cms-store.tsx`, `bulk-actions.ts`, and `cell-edit.ts`.
7
+ *
8
+ * Two real shapes exist over a document array and must stay distinct:
9
+ * - {@link replaceDoc}/{@link removeDoc} are pure, no-op-if-absent (an edit or
10
+ * delete of a document that's already gone from the session is a silent
11
+ * no-op, never an append).
12
+ * - {@link upsertDoc} is replace-or-append — `saveDocument` and
13
+ * `importDocuments` rely on the append branch to create new documents; a
14
+ * naive `replaceDoc` there would silently stop those flows from ever adding
15
+ * a document to the session.
16
+ */
17
+ /** Does `doc` identify the same document as `ref` (collection + id, EC-017)? */
18
+ export declare function matchesRef(doc: DocumentRef, ref: DocumentRef): boolean;
19
+ /**
20
+ * Replace the document matching `next`'s ref with `next`. A no-op (returns
21
+ * `docs` unchanged) if no document with that ref exists — pure map-replace,
22
+ * never appends.
23
+ */
24
+ export declare function replaceDoc(docs: readonly CmsDocument[], next: CmsDocument): CmsDocument[];
25
+ /** Remove the document identified by `ref`, if present. A no-op otherwise. */
26
+ export declare function removeDoc(docs: readonly CmsDocument[], ref: DocumentRef): CmsDocument[];
27
+ /**
28
+ * Replace the document matching `next`'s ref with `next`, or append it when
29
+ * no match exists (upsert) — the shape `saveDocument` and the accepted-batch
30
+ * fold of `importDocuments` need, so a create/import flow always lands the
31
+ * document even on a session with no prior entry for that ref.
32
+ */
33
+ export declare function upsertDoc(docs: readonly CmsDocument[], next: CmsDocument): CmsDocument[];
@@ -0,0 +1,47 @@
1
+ import { documentRefEquals } from '@elytracms/core/cms-core';
2
+ /**
3
+ * By-ref document array helpers (EC-326) — a shared idiom over
4
+ * `@elytracms/core/cms-core`'s `documentRefEquals`, replacing ~12 hand-inlined
5
+ * `d.collection === x.collection && d.id === x.id` predicates across
6
+ * `cms-store.tsx`, `bulk-actions.ts`, and `cell-edit.ts`.
7
+ *
8
+ * Two real shapes exist over a document array and must stay distinct:
9
+ * - {@link replaceDoc}/{@link removeDoc} are pure, no-op-if-absent (an edit or
10
+ * delete of a document that's already gone from the session is a silent
11
+ * no-op, never an append).
12
+ * - {@link upsertDoc} is replace-or-append — `saveDocument` and
13
+ * `importDocuments` rely on the append branch to create new documents; a
14
+ * naive `replaceDoc` there would silently stop those flows from ever adding
15
+ * a document to the session.
16
+ */
17
+ /** Does `doc` identify the same document as `ref` (collection + id, EC-017)? */
18
+ export function matchesRef(doc, ref) {
19
+ return documentRefEquals(doc, ref);
20
+ }
21
+ /**
22
+ * Replace the document matching `next`'s ref with `next`. A no-op (returns
23
+ * `docs` unchanged) if no document with that ref exists — pure map-replace,
24
+ * never appends.
25
+ */
26
+ export function replaceDoc(docs, next) {
27
+ return docs.map((d) => (matchesRef(d, next) ? next : d));
28
+ }
29
+ /** Remove the document identified by `ref`, if present. A no-op otherwise. */
30
+ export function removeDoc(docs, ref) {
31
+ return docs.filter((d) => !matchesRef(d, ref));
32
+ }
33
+ /**
34
+ * Replace the document matching `next`'s ref with `next`, or append it when
35
+ * no match exists (upsert) — the shape `saveDocument` and the accepted-batch
36
+ * fold of `importDocuments` need, so a create/import flow always lands the
37
+ * document even on a session with no prior entry for that ref.
38
+ */
39
+ export function upsertDoc(docs, next) {
40
+ const i = docs.findIndex((d) => matchesRef(d, next));
41
+ if (i < 0)
42
+ return [...docs, next];
43
+ const out = [...docs];
44
+ out[i] = next;
45
+ return out;
46
+ }
47
+ //# sourceMappingURL=document-ref-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-ref-utils.js","sourceRoot":"","sources":["../../../src/components/cms/document-ref-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAG5D;;;;;;;;;;;;;;GAcG;AAEH,gFAAgF;AAChF,MAAM,UAAU,UAAU,CAAC,GAAgB,EAAE,GAAgB;IAC3D,OAAO,iBAAiB,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;AACpC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,IAA4B,EAAE,IAAiB;IACxE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAC1D,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,SAAS,CAAC,IAA4B,EAAE,GAAgB;IACtE,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;AAChD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAC,IAA4B,EAAE,IAAiB;IACvE,MAAM,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAA;IACpD,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,CAAC,GAAG,IAAI,EAAE,IAAI,CAAC,CAAA;IACjC,MAAM,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,CAAA;IACrB,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAA;IACb,OAAO,GAAG,CAAA;AACZ,CAAC","sourcesContent":["import { documentRefEquals } from '@elytracms/core/cms-core'\nimport type { CmsDocument, DocumentRef } from '@elytracms/core/cms-core'\n\n/**\n * By-ref document array helpers (EC-326) — a shared idiom over\n * `@elytracms/core/cms-core`'s `documentRefEquals`, replacing ~12 hand-inlined\n * `d.collection === x.collection && d.id === x.id` predicates across\n * `cms-store.tsx`, `bulk-actions.ts`, and `cell-edit.ts`.\n *\n * Two real shapes exist over a document array and must stay distinct:\n * - {@link replaceDoc}/{@link removeDoc} are pure, no-op-if-absent (an edit or\n * delete of a document that's already gone from the session is a silent\n * no-op, never an append).\n * - {@link upsertDoc} is replace-or-append — `saveDocument` and\n * `importDocuments` rely on the append branch to create new documents; a\n * naive `replaceDoc` there would silently stop those flows from ever adding\n * a document to the session.\n */\n\n/** Does `doc` identify the same document as `ref` (collection + id, EC-017)? */\nexport function matchesRef(doc: DocumentRef, ref: DocumentRef): boolean {\n return documentRefEquals(doc, ref)\n}\n\n/**\n * Replace the document matching `next`'s ref with `next`. A no-op (returns\n * `docs` unchanged) if no document with that ref exists — pure map-replace,\n * never appends.\n */\nexport function replaceDoc(docs: readonly CmsDocument[], next: CmsDocument): CmsDocument[] {\n return docs.map((d) => (matchesRef(d, next) ? next : d))\n}\n\n/** Remove the document identified by `ref`, if present. A no-op otherwise. */\nexport function removeDoc(docs: readonly CmsDocument[], ref: DocumentRef): CmsDocument[] {\n return docs.filter((d) => !matchesRef(d, ref))\n}\n\n/**\n * Replace the document matching `next`'s ref with `next`, or append it when\n * no match exists (upsert) — the shape `saveDocument` and the accepted-batch\n * fold of `importDocuments` need, so a create/import flow always lands the\n * document even on a session with no prior entry for that ref.\n */\nexport function upsertDoc(docs: readonly CmsDocument[], next: CmsDocument): CmsDocument[] {\n const i = docs.findIndex((d) => matchesRef(d, next))\n if (i < 0) return [...docs, next]\n const out = [...docs]\n out[i] = next\n return out\n}\n"]}
@@ -1,7 +1,12 @@
1
1
  import type { CmsDocument } from '@elytracms/core/cms-core';
2
- /** Stage a document for the next `CmsProvider` mount of `projectId` (playground only). */
2
+ /** Stage a document for the next `CmsProvider` mount of `projectId` (any mode). */
3
3
  export declare function stageDocument(projectId: string, doc: CmsDocument): void;
4
- /** The documents staged for `projectId` (a copy; non-destructive). */
5
- export declare function stagedDocuments(projectId: string | undefined): CmsDocument[];
4
+ /**
5
+ * Merge any documents staged for `projectId` into a fresh mount's
6
+ * seeded/hydrated document list. See the module doc for why the merge rule
7
+ * itself is mode-aware (playground: staged always wins; Convex: staged fills
8
+ * gaps only, dropped once superseded).
9
+ */
10
+ export declare function consumeStagedDocuments(projectId: string | undefined, seeded: readonly CmsDocument[]): CmsDocument[];
6
11
  /** Test seam: drop all staged documents. */
7
12
  export declare function clearStagedDocuments(): void;
@@ -1,41 +1,101 @@
1
1
  import { isPlaygroundMode } from '@elytracms/studio/lib/persistence';
2
2
  /**
3
- * EC-320 — a tiny cross-mount handoff for a freshly created document.
3
+ * EC-320 (0.0.44 follow-up) — a tiny cross-mount handoff for a freshly created
4
+ * (or freshly appended-to) document.
4
5
  *
5
- * The "Use in document…" transfer creates a draft in the Components view, then
6
- * navigates to that document's CMS editor — a FRESH `CmsProvider` mount. In
7
- * **Convex** mode the new provider re-hydrates from persistence (the transfer's
8
- * `saveDocument` already upserted it), so nothing more is needed. In the
9
- * **playground**, though, `CmsSessionGate` renders synchronously with `null`
10
- * (fixtures) and never reads back the seeded adapter, so a doc created in one
11
- * mount is invisible to the next.
6
+ * The "Use in document…" transfer creates/updates a draft in the Components
7
+ * view, then navigates to that document's CMS editor — a FRESH `CmsProvider`
8
+ * mount. That fresh mount seeds its documents from whatever hydration/kit
9
+ * snapshot its `CmsSessionGate` resolves, and in BOTH modes that snapshot can
10
+ * miss a document the transfer just wrote — for two DIFFERENT reasons:
12
11
  *
13
- * This module bridges exactly that gap: the transfer STAGES the built document,
14
- * and the next `CmsProvider` for the same project merges any staged docs into its
15
- * initial set (staged wins by id). It is deliberately **playground-only** — under
16
- * Convex, hydration is the source of truth and staging would risk shadowing a
17
- * newer persisted revision. Reads are non-destructive (StrictMode-safe: a double
18
- * mount sees the same doc both times); the buffer is session-scoped dev state.
12
+ * - **Playground**: `CmsSessionGate` renders synchronously with `session:
13
+ * null` (the EC-138 determinism contract) — it never queries anything, so
14
+ * the seeded set is always the STATIC kit/fixture baseline, forever. It
15
+ * never reflects a write, on the first mount or the hundredth.
16
+ * - **Convex**: the seeded set comes from a REAL fresh-per-mount query
17
+ * (`loadCms()`), so it usually catches up — but there is no synchronous
18
+ * guarantee it observes a write from a moment earlier (belt and braces: the
19
+ * transfer also AWAITS its own write landing — see `transfer-dialog.tsx` —
20
+ * but staging covers the mount regardless of that timing).
21
+ *
22
+ * A previous version of this module gated `stageDocument` itself on
23
+ * `isPlaygroundMode()`, reasoning that Convex mode "re-hydrates from
24
+ * persistence" per mount and so never needs staging. That reasoning was
25
+ * wrong — a fresh query has no synchronous ordering guarantee against a write
26
+ * from moments earlier — and the gate was exactly why the Convex-mode
27
+ * transfer 404'd (EC-320 follow-up finding). Staging now runs in every mode.
28
+ *
29
+ * The two modes DO still genuinely differ on the read side, though — not by a
30
+ * wrong assumption this time, but structurally:
31
+ *
32
+ * - Playground's seeded set can NEVER be "fresher" than a staged write (it
33
+ * never changes at all), so a staged entry always wins over it — exactly
34
+ * the original, working behavior, now also covering "insert into an
35
+ * EXISTING document" (whose key was already in the static baseline before
36
+ * the edit — the append would otherwise be invisible).
37
+ * - Convex's seeded set is a genuine live signal, so a staged entry only
38
+ * fills a GAP it doesn't already carry; once it does, the seeded set wins
39
+ * outright (the newer-or-equal persisted truth) and the stale staged entry
40
+ * is dropped — so it can never resurrect on a later mount and shadow a
41
+ * revision made after the transfer (e.g. a subsequent edit, once persisted
42
+ * and re-hydrated).
43
+ *
44
+ * Reads that don't drop anything are non-destructive (StrictMode-safe: a
45
+ * double mount computes the identical merge both times); the buffer itself is
46
+ * in-memory, session-scoped runtime state — never persisted, never a
47
+ * substitute for the real write.
19
48
  */
20
49
  const staged = new Map();
21
50
  function docKey(doc) {
22
51
  return `${doc.collection}/${doc.id}`;
23
52
  }
24
- /** Stage a document for the next `CmsProvider` mount of `projectId` (playground only). */
53
+ /** Stage a document for the next `CmsProvider` mount of `projectId` (any mode). */
25
54
  export function stageDocument(projectId, doc) {
26
- if (!isPlaygroundMode())
27
- return;
28
55
  const list = staged.get(projectId) ?? [];
29
56
  // Replace any prior staging of the same doc key so a re-transfer wins.
30
57
  const next = list.filter((d) => docKey(d) !== docKey(doc));
31
58
  next.push(doc);
32
59
  staged.set(projectId, next);
33
60
  }
34
- /** The documents staged for `projectId` (a copy; non-destructive). */
35
- export function stagedDocuments(projectId) {
61
+ /**
62
+ * Merge any documents staged for `projectId` into a fresh mount's
63
+ * seeded/hydrated document list. See the module doc for why the merge rule
64
+ * itself is mode-aware (playground: staged always wins; Convex: staged fills
65
+ * gaps only, dropped once superseded).
66
+ */
67
+ export function consumeStagedDocuments(projectId, seeded) {
36
68
  if (projectId === undefined)
37
- return [];
38
- return (staged.get(projectId) ?? []).map((d) => structuredClone(d));
69
+ return [...seeded];
70
+ const list = staged.get(projectId);
71
+ if (!list || list.length === 0)
72
+ return [...seeded];
73
+ if (isPlaygroundMode()) {
74
+ // The seeded set is a static kit/fixture baseline that never reflects a
75
+ // write on its own — a staged doc always wins for its key, matching the
76
+ // ONLY mechanism that ever represents a live edit across mounts here.
77
+ const byKey = new Map(seeded.map((d) => [docKey(d), d]));
78
+ for (const d of list)
79
+ byKey.set(docKey(d), structuredClone(d));
80
+ return [...byKey.values()];
81
+ }
82
+ // Convex: `seeded` is a fresh per-mount hydration query — a real signal of
83
+ // the current persisted truth. A staged doc only fills a gap that snapshot
84
+ // doesn't already carry; a key it DOES carry is superseded and dropped from
85
+ // the buffer so it can never resurrect on a later mount.
86
+ const seededKeys = new Set(seeded.map(docKey));
87
+ const superseded = list.filter((d) => seededKeys.has(docKey(d)));
88
+ if (superseded.length > 0) {
89
+ const remaining = list.filter((d) => !superseded.some((s) => docKey(s) === docKey(d)));
90
+ if (remaining.length > 0)
91
+ staged.set(projectId, remaining);
92
+ else
93
+ staged.delete(projectId);
94
+ }
95
+ const gap = list.filter((d) => !seededKeys.has(docKey(d)));
96
+ if (gap.length === 0)
97
+ return [...seeded];
98
+ return [...seeded, ...gap.map((d) => structuredClone(d))];
39
99
  }
40
100
  /** Test seam: drop all staged documents. */
41
101
  export function clearStagedDocuments() {
@@ -1 +1 @@
1
- {"version":3,"file":"document-staging.js","sourceRoot":"","sources":["../../../src/components/cms/document-staging.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAA;AAEpE;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,MAAM,GAAG,IAAI,GAAG,EAAyB,CAAA;AAE/C,SAAS,MAAM,CAAC,GAAgB;IAC9B,OAAO,GAAG,GAAG,CAAC,UAAU,IAAI,GAAG,CAAC,EAAE,EAAE,CAAA;AACtC,CAAC;AAED,0FAA0F;AAC1F,MAAM,UAAU,aAAa,CAAC,SAAiB,EAAE,GAAgB;IAC/D,IAAI,CAAC,gBAAgB,EAAE;QAAE,OAAM;IAC/B,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,CAAA;IACxC,uEAAuE;IACvE,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;IAC1D,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IACd,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,CAAA;AAC7B,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,eAAe,CAAC,SAA6B;IAC3D,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,EAAE,CAAA;IACtC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAA;AACrE,CAAC;AAED,4CAA4C;AAC5C,MAAM,UAAU,oBAAoB;IAClC,MAAM,CAAC,KAAK,EAAE,CAAA;AAChB,CAAC","sourcesContent":["import type { CmsDocument } from '@elytracms/core/cms-core'\nimport { isPlaygroundMode } from '@elytracms/studio/lib/persistence'\n\n/**\n * EC-320 — a tiny cross-mount handoff for a freshly created document.\n *\n * The \"Use in document…\" transfer creates a draft in the Components view, then\n * navigates to that document's CMS editor — a FRESH `CmsProvider` mount. In\n * **Convex** mode the new provider re-hydrates from persistence (the transfer's\n * `saveDocument` already upserted it), so nothing more is needed. In the\n * **playground**, though, `CmsSessionGate` renders synchronously with `null`\n * (fixtures) and never reads back the seeded adapter, so a doc created in one\n * mount is invisible to the next.\n *\n * This module bridges exactly that gap: the transfer STAGES the built document,\n * and the next `CmsProvider` for the same project merges any staged docs into its\n * initial set (staged wins by id). It is deliberately **playground-only** — under\n * Convex, hydration is the source of truth and staging would risk shadowing a\n * newer persisted revision. Reads are non-destructive (StrictMode-safe: a double\n * mount sees the same doc both times); the buffer is session-scoped dev state.\n */\n\nconst staged = new Map<string, CmsDocument[]>()\n\nfunction docKey(doc: CmsDocument): string {\n return `${doc.collection}/${doc.id}`\n}\n\n/** Stage a document for the next `CmsProvider` mount of `projectId` (playground only). */\nexport function stageDocument(projectId: string, doc: CmsDocument): void {\n if (!isPlaygroundMode()) return\n const list = staged.get(projectId) ?? []\n // Replace any prior staging of the same doc key so a re-transfer wins.\n const next = list.filter((d) => docKey(d) !== docKey(doc))\n next.push(doc)\n staged.set(projectId, next)\n}\n\n/** The documents staged for `projectId` (a copy; non-destructive). */\nexport function stagedDocuments(projectId: string | undefined): CmsDocument[] {\n if (projectId === undefined) return []\n return (staged.get(projectId) ?? []).map((d) => structuredClone(d))\n}\n\n/** Test seam: drop all staged documents. */\nexport function clearStagedDocuments(): void {\n staged.clear()\n}\n"]}
1
+ {"version":3,"file":"document-staging.js","sourceRoot":"","sources":["../../../src/components/cms/document-staging.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAA;AAEpE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAEH,MAAM,MAAM,GAAG,IAAI,GAAG,EAAyB,CAAA;AAE/C,SAAS,MAAM,CAAC,GAAgB;IAC9B,OAAO,GAAG,GAAG,CAAC,UAAU,IAAI,GAAG,CAAC,EAAE,EAAE,CAAA;AACtC,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,aAAa,CAAC,SAAiB,EAAE,GAAgB;IAC/D,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,CAAA;IACxC,uEAAuE;IACvE,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;IAC1D,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IACd,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,CAAA;AAC7B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CACpC,SAA6B,EAC7B,MAA8B;IAE9B,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,CAAC,GAAG,MAAM,CAAC,CAAA;IAC/C,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;IAClC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,GAAG,MAAM,CAAC,CAAA;IAElD,IAAI,gBAAgB,EAAE,EAAE,CAAC;QACvB,wEAAwE;QACxE,wEAAwE;QACxE,sEAAsE;QACtE,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;QACxD,KAAK,MAAM,CAAC,IAAI,IAAI;YAAE,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,CAAA;QAC9D,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAA;IAC5B,CAAC;IAED,2EAA2E;IAC3E,2EAA2E;IAC3E,4EAA4E;IAC5E,yDAAyD;IACzD,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAA;IAC9C,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAChE,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1B,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QACtF,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC;YAAE,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;;YACrD,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;IAC/B,CAAC;IAED,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAC1D,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,GAAG,MAAM,CAAC,CAAA;IACxC,OAAO,CAAC,GAAG,MAAM,EAAE,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAC3D,CAAC;AAED,4CAA4C;AAC5C,MAAM,UAAU,oBAAoB;IAClC,MAAM,CAAC,KAAK,EAAE,CAAA;AAChB,CAAC","sourcesContent":["import type { CmsDocument } from '@elytracms/core/cms-core'\nimport { isPlaygroundMode } from '@elytracms/studio/lib/persistence'\n\n/**\n * EC-320 (0.0.44 follow-up) — a tiny cross-mount handoff for a freshly created\n * (or freshly appended-to) document.\n *\n * The \"Use in document…\" transfer creates/updates a draft in the Components\n * view, then navigates to that document's CMS editor — a FRESH `CmsProvider`\n * mount. That fresh mount seeds its documents from whatever hydration/kit\n * snapshot its `CmsSessionGate` resolves, and in BOTH modes that snapshot can\n * miss a document the transfer just wrote — for two DIFFERENT reasons:\n *\n * - **Playground**: `CmsSessionGate` renders synchronously with `session:\n * null` (the EC-138 determinism contract) — it never queries anything, so\n * the seeded set is always the STATIC kit/fixture baseline, forever. It\n * never reflects a write, on the first mount or the hundredth.\n * - **Convex**: the seeded set comes from a REAL fresh-per-mount query\n * (`loadCms()`), so it usually catches up — but there is no synchronous\n * guarantee it observes a write from a moment earlier (belt and braces: the\n * transfer also AWAITS its own write landing — see `transfer-dialog.tsx` —\n * but staging covers the mount regardless of that timing).\n *\n * A previous version of this module gated `stageDocument` itself on\n * `isPlaygroundMode()`, reasoning that Convex mode \"re-hydrates from\n * persistence\" per mount and so never needs staging. That reasoning was\n * wrong — a fresh query has no synchronous ordering guarantee against a write\n * from moments earlier — and the gate was exactly why the Convex-mode\n * transfer 404'd (EC-320 follow-up finding). Staging now runs in every mode.\n *\n * The two modes DO still genuinely differ on the read side, though — not by a\n * wrong assumption this time, but structurally:\n *\n * - Playground's seeded set can NEVER be \"fresher\" than a staged write (it\n * never changes at all), so a staged entry always wins over it — exactly\n * the original, working behavior, now also covering \"insert into an\n * EXISTING document\" (whose key was already in the static baseline before\n * the edit — the append would otherwise be invisible).\n * - Convex's seeded set is a genuine live signal, so a staged entry only\n * fills a GAP it doesn't already carry; once it does, the seeded set wins\n * outright (the newer-or-equal persisted truth) and the stale staged entry\n * is dropped — so it can never resurrect on a later mount and shadow a\n * revision made after the transfer (e.g. a subsequent edit, once persisted\n * and re-hydrated).\n *\n * Reads that don't drop anything are non-destructive (StrictMode-safe: a\n * double mount computes the identical merge both times); the buffer itself is\n * in-memory, session-scoped runtime state — never persisted, never a\n * substitute for the real write.\n */\n\nconst staged = new Map<string, CmsDocument[]>()\n\nfunction docKey(doc: CmsDocument): string {\n return `${doc.collection}/${doc.id}`\n}\n\n/** Stage a document for the next `CmsProvider` mount of `projectId` (any mode). */\nexport function stageDocument(projectId: string, doc: CmsDocument): void {\n const list = staged.get(projectId) ?? []\n // Replace any prior staging of the same doc key so a re-transfer wins.\n const next = list.filter((d) => docKey(d) !== docKey(doc))\n next.push(doc)\n staged.set(projectId, next)\n}\n\n/**\n * Merge any documents staged for `projectId` into a fresh mount's\n * seeded/hydrated document list. See the module doc for why the merge rule\n * itself is mode-aware (playground: staged always wins; Convex: staged fills\n * gaps only, dropped once superseded).\n */\nexport function consumeStagedDocuments(\n projectId: string | undefined,\n seeded: readonly CmsDocument[],\n): CmsDocument[] {\n if (projectId === undefined) return [...seeded]\n const list = staged.get(projectId)\n if (!list || list.length === 0) return [...seeded]\n\n if (isPlaygroundMode()) {\n // The seeded set is a static kit/fixture baseline that never reflects a\n // write on its own — a staged doc always wins for its key, matching the\n // ONLY mechanism that ever represents a live edit across mounts here.\n const byKey = new Map(seeded.map((d) => [docKey(d), d]))\n for (const d of list) byKey.set(docKey(d), structuredClone(d))\n return [...byKey.values()]\n }\n\n // Convex: `seeded` is a fresh per-mount hydration query — a real signal of\n // the current persisted truth. A staged doc only fills a gap that snapshot\n // doesn't already carry; a key it DOES carry is superseded and dropped from\n // the buffer so it can never resurrect on a later mount.\n const seededKeys = new Set(seeded.map(docKey))\n const superseded = list.filter((d) => seededKeys.has(docKey(d)))\n if (superseded.length > 0) {\n const remaining = list.filter((d) => !superseded.some((s) => docKey(s) === docKey(d)))\n if (remaining.length > 0) staged.set(projectId, remaining)\n else staged.delete(projectId)\n }\n\n const gap = list.filter((d) => !seededKeys.has(docKey(d)))\n if (gap.length === 0) return [...seeded]\n return [...seeded, ...gap.map((d) => structuredClone(d))]\n}\n\n/** Test seam: drop all staged documents. */\nexport function clearStagedDocuments(): void {\n staged.clear()\n}\n"]}
@@ -13,7 +13,7 @@ import { useOptionalAssets } from '../assets/assets-store';
13
13
  import { AssetThumb, FocalCropPreviews, FocalPointPicker, useAssetRecord } from '../assets/asset-thumb';
14
14
  import { newlyAddedAssetIds } from '../assets/focal-point';
15
15
  import { assetMetaFieldsOf, pruneMeta } from '../assets/asset-metadata';
16
- import { fieldLabel } from './cms-store';
16
+ import { fieldLabel } from './cms-labels';
17
17
  import { useCollectionView } from '../collection-view/collection-view-context';
18
18
  import { useFieldDensity } from './field-density';
19
19
  import { useOptionalDocumentEditor } from './document-editor-context';