@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
@@ -0,0 +1,75 @@
1
+ import { validateCompositionValue } from '@elytracms/core/project-graph';
2
+ import { collectComponentEmbeds, parseRichTextValue } from '@elytracms/core/rich-text';
3
+ import { getStudioRegistry } from '@elytracms/studio/lib/registry';
4
+ /**
5
+ * Composition validator adapters (EC-326, extracted from `cms-store.tsx`) —
6
+ * injected into `validateDocument`'s `validateComposition`/`validateRichTextEmbeds`
7
+ * options so `@elytracms/core/cms-core` stays free of `@elytracms/core/project-graph`
8
+ * and `@elytracms/core/rich-text`.
9
+ */
10
+ /**
11
+ * EC-188: validate a `blocks` composition field value against the field's `allow`
12
+ * vocabulary + the code-owned component registry (the leash), mapping graph
13
+ * issues into plain CMS findings. Never throws — a malformed (non-node)
14
+ * value degrades to a single finding, matching the "validation is first-class,
15
+ * never a crash" rule.
16
+ */
17
+ export function validateComposition(value, field) {
18
+ if (value == null)
19
+ return [];
20
+ const roots = Array.isArray(value) ? value : [value];
21
+ if (!roots.every((r) => r != null && typeof r === 'object' && 'componentId' in r)) {
22
+ return [{ message: 'is not a valid composition (expected component nodes)', severity: 'error' }];
23
+ }
24
+ const components = getStudioRegistry().registry.lookup;
25
+ return validateCompositionValue(value, {
26
+ allow: field.allow,
27
+ components,
28
+ }).map((issue) => ({
29
+ message: issue.message,
30
+ severity: issue.severity,
31
+ nodePath: issue.path.join('/'),
32
+ }));
33
+ }
34
+ /**
35
+ * EC-189: validate the composition embeds inside a `richText` field value. Each
36
+ * `componentEmbed` holds a single `ComponentNode` constrained by the field's
37
+ * `allow` vocabulary (the SAME leash a `blocks` field uses). Parse the value, walk
38
+ * its embeds at any depth, and validate each embedded node via
39
+ * `validateCompositionValue`; findings carry the rich-text node path joined with
40
+ * the in-embed graph path. Never throws — a malformed embed degrades to a
41
+ * single finding.
42
+ */
43
+ export function validateRichTextEmbeds(value, field) {
44
+ if (value == null)
45
+ return [];
46
+ const parsed = parseRichTextValue(value);
47
+ // A plain-string / schema-invalid rich-text value carries no embeds to check
48
+ // here (scalar type validation owns reporting a malformed value).
49
+ if (!parsed.ok)
50
+ return [];
51
+ const components = getStudioRegistry().registry.lookup;
52
+ const out = [];
53
+ for (const occurrence of collectComponentEmbeds(parsed.value.doc)) {
54
+ if (!occurrence.embedded) {
55
+ out.push({
56
+ message: `embed at ${occurrence.nodePath} is missing a valid embedded component`,
57
+ severity: 'error',
58
+ nodePath: occurrence.nodePath,
59
+ });
60
+ continue;
61
+ }
62
+ for (const issue of validateCompositionValue(occurrence.embedded, {
63
+ allow: field.allow,
64
+ components,
65
+ })) {
66
+ out.push({
67
+ message: issue.message,
68
+ severity: issue.severity,
69
+ nodePath: `${occurrence.nodePath}/attrs/node/${issue.path.join('/')}`,
70
+ });
71
+ }
72
+ }
73
+ return out;
74
+ }
75
+ //# sourceMappingURL=cms-composition-validators.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cms-composition-validators.js","sourceRoot":"","sources":["../../../src/components/cms/cms-composition-validators.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AAExE,OAAO,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AACtF,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAElE;;;;;GAKG;AAEH;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAc,EAAE,KAAqB;IACvE,IAAI,KAAK,IAAI,IAAI;QAAE,OAAO,EAAE,CAAA;IAC5B,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAA;IACpD,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,IAAI,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,aAAa,IAAI,CAAC,CAAC,EAAE,CAAC;QAClF,OAAO,CAAC,EAAE,OAAO,EAAE,uDAAuD,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAA;IAClG,CAAC;IACD,MAAM,UAAU,GAAG,iBAAiB,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAA;IACtD,OAAO,wBAAwB,CAAC,KAAwC,EAAE;QACxE,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,UAAU;KACX,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACjB,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;KAC/B,CAAC,CAAC,CAAA;AACL,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,sBAAsB,CAAC,KAAc,EAAE,KAAuB;IAC5E,IAAI,KAAK,IAAI,IAAI;QAAE,OAAO,EAAE,CAAA;IAC5B,MAAM,MAAM,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAA;IACxC,6EAA6E;IAC7E,kEAAkE;IAClE,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,OAAO,EAAE,CAAA;IACzB,MAAM,UAAU,GAAG,iBAAiB,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAA;IACtD,MAAM,GAAG,GAAuB,EAAE,CAAA;IAClC,KAAK,MAAM,UAAU,IAAI,sBAAsB,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QAClE,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC;YACzB,GAAG,CAAC,IAAI,CAAC;gBACP,OAAO,EAAE,YAAY,UAAU,CAAC,QAAQ,wCAAwC;gBAChF,QAAQ,EAAE,OAAO;gBACjB,QAAQ,EAAE,UAAU,CAAC,QAAQ;aAC9B,CAAC,CAAA;YACF,SAAQ;QACV,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,wBAAwB,CAAC,UAAU,CAAC,QAAQ,EAAE;YAChE,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,UAAU;SACX,CAAC,EAAE,CAAC;YACH,GAAG,CAAC,IAAI,CAAC;gBACP,OAAO,EAAE,KAAK,CAAC,OAAO;gBACtB,QAAQ,EAAE,KAAK,CAAC,QAAQ;gBACxB,QAAQ,EAAE,GAAG,UAAU,CAAC,QAAQ,eAAe,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE;aACtE,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC","sourcesContent":["import type { BlocksFieldDef, CompositionIssue, RichTextFieldDef } from '@elytracms/core/cms-core'\nimport { validateCompositionValue } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { collectComponentEmbeds, parseRichTextValue } from '@elytracms/core/rich-text'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\n\n/**\n * Composition validator adapters (EC-326, extracted from `cms-store.tsx`) —\n * injected into `validateDocument`'s `validateComposition`/`validateRichTextEmbeds`\n * options so `@elytracms/core/cms-core` stays free of `@elytracms/core/project-graph`\n * and `@elytracms/core/rich-text`.\n */\n\n/**\n * EC-188: validate a `blocks` composition field value against the field's `allow`\n * vocabulary + the code-owned component registry (the leash), mapping graph\n * issues into plain CMS findings. Never throws — a malformed (non-node)\n * value degrades to a single finding, matching the \"validation is first-class,\n * never a crash\" rule.\n */\nexport function validateComposition(value: unknown, field: BlocksFieldDef): CompositionIssue[] {\n if (value == null) return []\n const roots = Array.isArray(value) ? value : [value]\n if (!roots.every((r) => r != null && typeof r === 'object' && 'componentId' in r)) {\n return [{ message: 'is not a valid composition (expected component nodes)', severity: 'error' }]\n }\n const components = getStudioRegistry().registry.lookup\n return validateCompositionValue(value as ComponentNode | ComponentNode[], {\n allow: field.allow,\n components,\n }).map((issue) => ({\n message: issue.message,\n severity: issue.severity,\n nodePath: issue.path.join('/'),\n }))\n}\n\n/**\n * EC-189: validate the composition embeds inside a `richText` field value. Each\n * `componentEmbed` holds a single `ComponentNode` constrained by the field's\n * `allow` vocabulary (the SAME leash a `blocks` field uses). Parse the value, walk\n * its embeds at any depth, and validate each embedded node via\n * `validateCompositionValue`; findings carry the rich-text node path joined with\n * the in-embed graph path. Never throws — a malformed embed degrades to a\n * single finding.\n */\nexport function validateRichTextEmbeds(value: unknown, field: RichTextFieldDef): CompositionIssue[] {\n if (value == null) return []\n const parsed = parseRichTextValue(value)\n // A plain-string / schema-invalid rich-text value carries no embeds to check\n // here (scalar type validation owns reporting a malformed value).\n if (!parsed.ok) return []\n const components = getStudioRegistry().registry.lookup\n const out: CompositionIssue[] = []\n for (const occurrence of collectComponentEmbeds(parsed.value.doc)) {\n if (!occurrence.embedded) {\n out.push({\n message: `embed at ${occurrence.nodePath} is missing a valid embedded component`,\n severity: 'error',\n nodePath: occurrence.nodePath,\n })\n continue\n }\n for (const issue of validateCompositionValue(occurrence.embedded, {\n allow: field.allow,\n components,\n })) {\n out.push({\n message: issue.message,\n severity: issue.severity,\n nodePath: `${occurrence.nodePath}/attrs/node/${issue.path.join('/')}`,\n })\n }\n }\n return out\n}\n"]}
@@ -0,0 +1,15 @@
1
+ import type { FieldDef } from '@elytracms/core/cms-core';
2
+ /**
3
+ * Pure label formatters (EC-326) — no `CmsProvider`/store coupling, so
4
+ * portable surfaces (AD-19: `collection-table.tsx`, `detail-editor.tsx`, and
5
+ * friends) can format a field label without statically depending on the
6
+ * 1000+ line `cms-store.tsx` module for a single formatter.
7
+ */
8
+ /**
9
+ * A field's display label (EC-241): resolve its (possibly localized) `form.label`,
10
+ * falling back to the field name. **`locale` defaults to the active UI locale** —
11
+ * a bare `fieldLabel(field)` localizes a `{ en, de }` map against the browser locale
12
+ * (via the non-hook `localizedLabel`), so a German editor sees German labels. Pass an
13
+ * explicit `locale` only to force a specific one (e.g. a content-locale variant view).
14
+ */
15
+ export declare function fieldLabel(field: FieldDef, locale?: string): string;
@@ -0,0 +1,27 @@
1
+ import { resolveLabel } from '@elytracms/core/cms-core';
2
+ import { localizedLabel, resolveMsgLabel } from '@elytracms/studio/lib/i18n';
3
+ /**
4
+ * Pure label formatters (EC-326) — no `CmsProvider`/store coupling, so
5
+ * portable surfaces (AD-19: `collection-table.tsx`, `detail-editor.tsx`, and
6
+ * friends) can format a field label without statically depending on the
7
+ * 1000+ line `cms-store.tsx` module for a single formatter.
8
+ */
9
+ /**
10
+ * A field's display label (EC-241): resolve its (possibly localized) `form.label`,
11
+ * falling back to the field name. **`locale` defaults to the active UI locale** —
12
+ * a bare `fieldLabel(field)` localizes a `{ en, de }` map against the browser locale
13
+ * (via the non-hook `localizedLabel`), so a German editor sees German labels. Pass an
14
+ * explicit `locale` only to force a specific one (e.g. a content-locale variant view).
15
+ */
16
+ export function fieldLabel(field, locale) {
17
+ // A catalog-backed built-in-rail label (msgLabel) always resolves through the
18
+ // catalog against the active UI locale, ignoring a content-locale override — a rail
19
+ // is chrome, not localized content.
20
+ const chrome = resolveMsgLabel(field.form?.label);
21
+ if (chrome !== undefined)
22
+ return chrome;
23
+ if (locale !== undefined)
24
+ return resolveLabel(field.form?.label, locale) ?? field.name;
25
+ return localizedLabel(field.form?.label, field.name);
26
+ }
27
+ //# sourceMappingURL=cms-labels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cms-labels.js","sourceRoot":"","sources":["../../../src/components/cms/cms-labels.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACvD,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AAE5E;;;;;GAKG;AAEH;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,KAAe,EAAE,MAAe;IACzD,8EAA8E;IAC9E,oFAAoF;IACpF,oCAAoC;IACpC,MAAM,MAAM,GAAG,eAAe,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;IACjD,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAA;IACvC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,YAAY,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,IAAI,KAAK,CAAC,IAAI,CAAA;IACtF,OAAO,cAAc,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,CAAA;AACtD,CAAC","sourcesContent":["import type { FieldDef } from '@elytracms/core/cms-core'\nimport { resolveLabel } from '@elytracms/core/cms-core'\nimport { localizedLabel, resolveMsgLabel } from '@elytracms/studio/lib/i18n'\n\n/**\n * Pure label formatters (EC-326) — no `CmsProvider`/store coupling, so\n * portable surfaces (AD-19: `collection-table.tsx`, `detail-editor.tsx`, and\n * friends) can format a field label without statically depending on the\n * 1000+ line `cms-store.tsx` module for a single formatter.\n */\n\n/**\n * A field's display label (EC-241): resolve its (possibly localized) `form.label`,\n * falling back to the field name. **`locale` defaults to the active UI locale** —\n * a bare `fieldLabel(field)` localizes a `{ en, de }` map against the browser locale\n * (via the non-hook `localizedLabel`), so a German editor sees German labels. Pass an\n * explicit `locale` only to force a specific one (e.g. a content-locale variant view).\n */\nexport function fieldLabel(field: FieldDef, locale?: string): string {\n // A catalog-backed built-in-rail label (msgLabel) always resolves through the\n // catalog against the active UI locale, ignoring a content-locale override — a rail\n // is chrome, not localized content.\n const chrome = resolveMsgLabel(field.form?.label)\n if (chrome !== undefined) return chrome\n if (locale !== undefined) return resolveLabel(field.form?.label, locale) ?? field.name\n return localizedLabel(field.form?.label, field.name)\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from 'react';
2
- import type { CmsDocument, CmsValidationIssue, CollectionDef, DocumentRef, FieldDef, Locale, LocaleConfig, RestoreScope } from '@elytracms/core/cms-core';
2
+ import type { CmsDocument, CmsValidationIssue, CollectionDef, CollectionRegistry, DocumentRef, FieldDef, Locale, LocaleConfig, RestoreScope } from '@elytracms/core/cms-core';
3
3
  import type { DocumentVersionRecord } from '@elytracms/core/persistence';
4
4
  import type { OperationError } from '@elytracms/studio/lib/operations';
5
5
  import type { TableViewState } from '@elytracms/studio/lib/cms-views';
@@ -142,6 +142,7 @@ export interface CmsStore {
142
142
  /** Await all queued persisted mutations (tests, publish flows). */
143
143
  flushPersistence(): Promise<void>;
144
144
  }
145
+ declare const useCms: () => CmsStore;
145
146
  declare function primaryField(collection: CollectionDef): string;
146
147
  /**
147
148
  * EC-316 (bug fix) — mint a fresh, COLLISION-FREE document id for `+ New`.
@@ -158,6 +159,26 @@ declare function primaryField(collection: CollectionDef): string;
158
159
  export declare function mintDocumentId(collectionId: string, seq: {
159
160
  current: number;
160
161
  }, existingIds: ReadonlySet<string>): string;
162
+ /**
163
+ * Build a fresh document for `collectionId`: a collision-free id (EC-316, via
164
+ * {@link mintDocumentId} against every id `existingDocs` already holds for
165
+ * this collection) and the collection's declared field defaults (EC-297, via
166
+ * `seedDefaults`). Pure — the sensitive part of the insert sequence shared by
167
+ * `createDocument` and `ensureSingletonDocument`'s empty branch (via the
168
+ * private `insertNewDocument` below), factored out so it's unit-testable
169
+ * without a `CmsProvider`.
170
+ */
171
+ export declare function buildNewDocument(collectionId: string, registry: CollectionRegistry, seq: {
172
+ current: number;
173
+ }, existingDocs: readonly CmsDocument[]): CmsDocument;
174
+ /**
175
+ * Does `docs` already hold a document for `collectionId`? `ensureSingletonDocument`'s
176
+ * empty branch re-checks this INSIDE the `setDocuments` updater (against the
177
+ * latest `prev`, not a stale snapshot) so a concurrent invocation — StrictMode's
178
+ * double-invoked effect, or a second caller racing the same synchronous guard —
179
+ * can never materialize a second singleton document.
180
+ */
181
+ export declare function hasDocumentFor(docs: readonly CmsDocument[], collectionId: string): boolean;
161
182
  /**
162
183
  * The CMS session store (EC-141): documents and collections are optimistic,
163
184
  * synchronous session state (live validation while typing); every mutation —
@@ -181,14 +202,6 @@ export declare function CmsProvider({ children, projectId, initialCollections, i
181
202
  /** Locale config for the seeded content; defaults to the sample config. */
182
203
  initialLocaleConfig?: LocaleConfig;
183
204
  }): import("react").JSX.Element;
184
- export declare function useCms(): CmsStore;
185
- /**
186
- * A field's display label (EC-241): resolve its (possibly localized) `form.label`,
187
- * falling back to the field name. **`locale` defaults to the active UI locale** —
188
- * a bare `fieldLabel(field)` localizes a `{ en, de }` map against the browser locale
189
- * (via the non-hook `localizedLabel`), so a German editor sees German labels. Pass an
190
- * explicit `locale` only to force a specific one (e.g. a content-locale variant view).
191
- */
192
- export declare function fieldLabel(field: FieldDef, locale?: string): string;
205
+ export { useCms };
193
206
  export { primaryField };
194
207
  export type { Locale };
@@ -1,86 +1,20 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
3
- import { createCollectionRegistry, createDocumentStore, documentKey, documentSchema, resolveDocumentTitle, resolveField, resolveLabel, sampleDocuments, seedDefaults, sampleFilterableCollections, sampleLocaleConfig, samplePublishedKeys, resolveSingletonDocument, titleFieldOf, validateDocument, } from '@elytracms/core/cms-core';
4
- import { validateCompositionValue } from '@elytracms/core/project-graph';
5
- import { collectComponentEmbeds, parseRichTextValue } from '@elytracms/core/rich-text';
6
- import { localizedLabel, resolveMsgLabel, translate } from '@elytracms/studio/lib/i18n';
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ import { createCollectionRegistry, createDocumentStore, documentKey, documentSchema, resolveDocumentTitle, resolveField, sampleDocuments, seedDefaults, sampleFilterableCollections, sampleLocaleConfig, samplePublishedKeys, resolveSingletonDocument, titleFieldOf, validateDocument, } from '@elytracms/core/cms-core';
4
+ import { translate } from '@elytracms/studio/lib/i18n';
7
5
  import { configureStudioLocales, useStudioLocale } from '@elytracms/studio/lib/locale';
8
- import { getStudioRegistry, studioManifestLookup } from '@elytracms/studio/lib/registry';
6
+ import { studioManifestLookup } from '@elytracms/studio/lib/registry';
9
7
  import { resolveDeploymentCollections } from '@elytracms/studio/lib/workspace';
8
+ import { validateComposition, validateRichTextEmbeds } from './cms-composition-validators';
9
+ import { matchesRef, removeDoc, replaceDoc, upsertDoc } from './document-ref-utils';
10
+ import { createRequiredContext } from '@elytracms/studio/lib/required-context';
10
11
  import { createCmsPersister, getStudioOperations } from '@elytracms/studio/lib/operations';
11
12
  import { ALL_VIEW_ID, canonicalViewState, configViewsFor, defaultViewState, viewStatesEqual, } from '@elytracms/studio/lib/cms-views';
12
13
  import { useOptionalAssets } from '../assets/assets-store';
13
- import { stagedDocuments } from './document-staging';
14
+ import { consumeStagedDocuments } from './document-staging';
14
15
  import { applyCellValue, commitCellEdit } from './cell-edit';
15
16
  import { runBulkAction } from './bulk-actions';
16
- /**
17
- * EC-188: validate a `blocks` composition field value against the field's `allow`
18
- * vocabulary + the code-owned component registry (the leash), mapping graph
19
- * issues into plain CMS findings. Injected into `validateDocument` so cms-core
20
- * stays free of `@elytracms/core/project-graph`. Never throws — a malformed (non-node)
21
- * value degrades to a single finding, matching the "validation is first-class,
22
- * never a crash" rule.
23
- */
24
- function validateComposition(value, field) {
25
- if (value == null)
26
- return [];
27
- const roots = Array.isArray(value) ? value : [value];
28
- if (!roots.every((r) => r != null && typeof r === 'object' && 'componentId' in r)) {
29
- return [{ message: 'is not a valid composition (expected component nodes)', severity: 'error' }];
30
- }
31
- const components = getStudioRegistry().registry.lookup;
32
- return validateCompositionValue(value, {
33
- allow: field.allow,
34
- components,
35
- }).map((issue) => ({
36
- message: issue.message,
37
- severity: issue.severity,
38
- nodePath: issue.path.join('/'),
39
- }));
40
- }
41
- /**
42
- * EC-189: validate the composition embeds inside a `richText` field value. Each
43
- * `componentEmbed` holds a single `ComponentNode` constrained by the field's
44
- * `allow` vocabulary (the SAME leash a `blocks` field uses). Parse the value, walk
45
- * its embeds at any depth, and validate each embedded node via
46
- * `validateCompositionValue`; findings carry the rich-text node path joined with
47
- * the in-embed graph path. Injected into `validateDocument` so cms-core depends on
48
- * neither `@elytracms/core/project-graph` nor `@elytracms/core/rich-text`. Never throws — a
49
- * malformed embed degrades to a single finding.
50
- */
51
- function validateRichTextEmbeds(value, field) {
52
- if (value == null)
53
- return [];
54
- const parsed = parseRichTextValue(value);
55
- // A plain-string / schema-invalid rich-text value carries no embeds to check
56
- // here (scalar type validation owns reporting a malformed value).
57
- if (!parsed.ok)
58
- return [];
59
- const components = getStudioRegistry().registry.lookup;
60
- const out = [];
61
- for (const occurrence of collectComponentEmbeds(parsed.value.doc)) {
62
- if (!occurrence.embedded) {
63
- out.push({
64
- message: `embed at ${occurrence.nodePath} is missing a valid embedded component`,
65
- severity: 'error',
66
- nodePath: occurrence.nodePath,
67
- });
68
- continue;
69
- }
70
- for (const issue of validateCompositionValue(occurrence.embedded, {
71
- allow: field.allow,
72
- components,
73
- })) {
74
- out.push({
75
- message: issue.message,
76
- severity: issue.severity,
77
- nodePath: `${occurrence.nodePath}/attrs/node/${issue.path.join('/')}`,
78
- });
79
- }
80
- }
81
- return out;
82
- }
83
- const CmsContext = createContext(null);
17
+ const { Context: CmsContext, useRequired: useCms } = createRequiredContext('Cms');
84
18
  function primaryField(collection) {
85
19
  return titleFieldOf(collection)?.name ?? collection.fields[0]?.name ?? 'id';
86
20
  }
@@ -104,6 +38,31 @@ export function mintDocumentId(collectionId, seq, existingIds) {
104
38
  } while (existingIds.has(id));
105
39
  return id;
106
40
  }
41
+ /**
42
+ * Build a fresh document for `collectionId`: a collision-free id (EC-316, via
43
+ * {@link mintDocumentId} against every id `existingDocs` already holds for
44
+ * this collection) and the collection's declared field defaults (EC-297, via
45
+ * `seedDefaults`). Pure — the sensitive part of the insert sequence shared by
46
+ * `createDocument` and `ensureSingletonDocument`'s empty branch (via the
47
+ * private `insertNewDocument` below), factored out so it's unit-testable
48
+ * without a `CmsProvider`.
49
+ */
50
+ export function buildNewDocument(collectionId, registry, seq, existingDocs) {
51
+ const existingIds = new Set(existingDocs.filter((d) => d.collection === collectionId).map((d) => d.id));
52
+ const id = mintDocumentId(collectionId, seq, existingIds);
53
+ const collection = registry.get(collectionId);
54
+ return { collection: collectionId, id, values: collection ? seedDefaults(collection) : {} };
55
+ }
56
+ /**
57
+ * Does `docs` already hold a document for `collectionId`? `ensureSingletonDocument`'s
58
+ * empty branch re-checks this INSIDE the `setDocuments` updater (against the
59
+ * latest `prev`, not a stale snapshot) so a concurrent invocation — StrictMode's
60
+ * double-invoked effect, or a second caller racing the same synchronous guard —
61
+ * can never materialize a second singleton document.
62
+ */
63
+ export function hasDocumentFor(docs, collectionId) {
64
+ return docs.some((d) => d.collection === collectionId);
65
+ }
107
66
  /** The unnamed built-in view: no filters, no sort, every field visible. */
108
67
  function defaultView(collection) {
109
68
  return {
@@ -124,16 +83,14 @@ function defaultView(collection) {
124
83
  export function CmsProvider({ children, projectId, initialCollections, initialDocuments, initialPublishedKeys, initialLocaleConfig, }) {
125
84
  const [documents, setDocuments] = useState(() => {
126
85
  const seeded = (initialDocuments ?? sampleDocuments).map((d) => structuredClone(d));
127
- // EC-320: merge any documents STAGED by the Components-view "Use in document…"
128
- // transfer (a playground cross-mount handoff — see document-staging). Staged
129
- // wins by id; empty in Convex mode, where hydration is the source of truth.
130
- const staged = stagedDocuments(projectId);
131
- if (staged.length === 0)
132
- return seeded;
133
- const byKey = new Map(seeded.map((d) => [`${d.collection}/${d.id}`, d]));
134
- for (const d of staged)
135
- byKey.set(`${d.collection}/${d.id}`, d);
136
- return [...byKey.values()];
86
+ // EC-320 (0.0.44 follow-up): merge any documents STAGED by the
87
+ // Components-view "Use in document…" transfer — a cross-mount handoff in
88
+ // EVERY mode now (see document-staging.ts), not just the playground. The
89
+ // merge rule itself stays mode-aware there: playground's seeded set is a
90
+ // static baseline that never reflects a write (staged always wins);
91
+ // Convex's is a fresh per-mount hydration query (staged fills gaps only,
92
+ // dropped once the seeded set actually carries the key).
93
+ return consumeStagedDocuments(projectId, seeded);
137
94
  });
138
95
  // Published-ness as the version POINTER (EC-224), decoupled from the flat
139
96
  // `state` enum: seeded from hydration's pointer set, or — when none is supplied
@@ -209,9 +166,9 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
209
166
  configureStudioLocales(localeConfig);
210
167
  }, [localeConfig]);
211
168
  const getCollection = useCallback((id) => registry.get(id), [registry]);
212
- const getDocument = useCallback((ref) => documents.find((d) => d.collection === ref.collection && d.id === ref.id), [documents]);
169
+ const getDocument = useCallback((ref) => documents.find((d) => matchesRef(d, ref)), [documents]);
213
170
  const titleOf = useCallback((ref) => {
214
- const doc = documents.find((d) => d.collection === ref.collection && d.id === ref.id);
171
+ const doc = documents.find((d) => matchesRef(d, ref));
215
172
  const collection = registry.get(ref.collection);
216
173
  if (!doc || !collection)
217
174
  return ref.id;
@@ -232,18 +189,15 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
232
189
  return typeof value === 'string' && value ? value : ref.id;
233
190
  }, [documents, registry, localeConfig, activeLocale]);
234
191
  const saveDocument = useCallback((doc) => {
235
- setDocuments((prev) => {
236
- const i = prev.findIndex((d) => d.collection === doc.collection && d.id === doc.id);
237
- if (i < 0)
238
- return [...prev, doc];
239
- const next = [...prev];
240
- next[i] = doc;
241
- return next;
242
- });
192
+ // Replace-or-append (upsert): a brand-new document must still land even
193
+ // though nothing in the session matches its ref yet.
194
+ setDocuments((prev) => upsertDoc(prev, doc));
243
195
  persisterRef.current?.upsertDocument(doc);
244
196
  }, []);
245
197
  const commitFieldEdit = useCallback((input) => {
246
- const replaceWith = (doc) => setDocuments((prev) => prev.map((d) => (d.collection === doc.collection && d.id === doc.id ? doc : d)));
198
+ // Pure map-replace: an edit always targets a document already open in
199
+ // the session, so a miss (already gone) is a no-op, never an append.
200
+ const replaceWith = (doc) => setDocuments((prev) => replaceDoc(prev, doc));
247
201
  const persister = persisterRef.current;
248
202
  return commitCellEdit({
249
203
  registry,
@@ -264,22 +218,24 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
264
218
  : {}),
265
219
  }, input);
266
220
  }, [registry, localeConfig, projectId]);
267
- const createDocument = useCallback((collectionId) => {
268
- // EC-316: never reissue an id a document (this session's or a prior
269
- // one's) already holds — see `mintDocumentId`.
270
- const existingIds = new Set(documentsRef.current.filter((d) => d.collection === collectionId).map((d) => d.id));
271
- const id = mintDocumentId(collectionId, seq, existingIds);
272
- // EC-297: seed the collection's declared field defaults at create time.
273
- const collection = registry.get(collectionId);
274
- const doc = {
275
- collection: collectionId,
276
- id,
277
- values: collection ? seedDefaults(collection) : {},
278
- };
279
- setDocuments((prev) => [...prev, doc]);
221
+ // Shared insert sequence (EC-326): existingIds/mint/seedDefaults (via
222
+ // `buildNewDocument`) + optimistic append + persist, consumed by both
223
+ // `createDocument` (always inserts) and `ensureSingletonDocument`'s empty
224
+ // branch (`guardExisting: true` — re-checks "no document for this collection
225
+ // yet" against the LATEST `prev` inside the `setDocuments` updater, not the
226
+ // `documentsRef.current` snapshot the caller already checked, so a
227
+ // concurrent invocation — StrictMode's double-invoked effect — can never
228
+ // materialize a second singleton document).
229
+ const insertNewDocument = useCallback((collectionId, options) => {
230
+ const doc = buildNewDocument(collectionId, registry, seq, documentsRef.current);
231
+ setDocuments((prev) => options?.guardExisting && hasDocumentFor(prev, collectionId) ? prev : [...prev, doc]);
280
232
  persisterRef.current?.upsertDocument(doc);
281
- return { collection: collectionId, id };
233
+ return doc;
282
234
  }, [registry]);
235
+ const createDocument = useCallback((collectionId) => {
236
+ const doc = insertNewDocument(collectionId);
237
+ return { collection: collectionId, id: doc.id };
238
+ }, [insertNewDocument]);
283
239
  // EC-274 (re-scopes EC-217): a singleton is a UI lens, not a fixed id. Resolve
284
240
  // the collection's lone document and return its ref; if none exists yet, create
285
241
  // one with a NORMAL generated id (never the collection id — no code ever names a
@@ -308,22 +264,14 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
308
264
  return { collection: collectionId, id: `new-${collectionId}-pending` };
309
265
  }
310
266
  ensuredSingletons.current.add(collectionId);
311
- // EC-316: same collision-free mint as `createDocument` (defensive here —
312
- // the singleton-empty branch only runs when no document exists yet).
313
- const existingIds = new Set(documentsRef.current.filter((d) => d.collection === collectionId).map((d) => d.id));
314
- const id = mintDocumentId(collectionId, seq, existingIds);
315
- // EC-297: seed the singleton's declared field defaults at create time.
316
- const doc = {
317
- collection: collectionId,
318
- id,
319
- values: collection ? seedDefaults(collection) : {},
320
- };
321
- setDocuments((prev) => prev.some((d) => d.collection === collectionId) ? prev : [...prev, doc]);
322
- persisterRef.current?.upsertDocument(doc);
323
- return { collection: collectionId, id };
324
- }, [registry]);
267
+ // EC-316: same collision-free mint as `createDocument` (via the shared
268
+ // `insertNewDocument` — defensive here, the singleton-empty branch only
269
+ // runs when no document exists yet).
270
+ const doc = insertNewDocument(collectionId, { guardExisting: true });
271
+ return { collection: collectionId, id: doc.id };
272
+ }, [registry, insertNewDocument]);
325
273
  const deleteDocument = useCallback((ref) => {
326
- setDocuments((prev) => prev.filter((d) => !(d.collection === ref.collection && d.id === ref.id)));
274
+ setDocuments((prev) => removeDoc(prev, ref));
327
275
  persisterRef.current?.deleteDocument(ref);
328
276
  }, []);
329
277
  // EC-224: published-ness IS the version pointer (`publishedKeys`) — the SOLE
@@ -412,7 +360,9 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
412
360
  return null;
413
361
  }
414
362
  const restored = result.value.document;
415
- setDocuments((prev) => prev.map((d) => d.collection === restored.collection && d.id === restored.id ? restored : d));
363
+ // Pure map-replace: a restore always targets a document already open in
364
+ // the session.
365
+ setDocuments((prev) => replaceDoc(prev, restored));
416
366
  return restored;
417
367
  }, [projectId]);
418
368
  const validate = useCallback((doc) => validateDocument(registry, doc, {
@@ -462,17 +412,10 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
462
412
  accepted.push(doc);
463
413
  }
464
414
  if (accepted.length > 0) {
465
- setDocuments((prev) => {
466
- const next = [...prev];
467
- for (const doc of accepted) {
468
- const i = next.findIndex((d) => d.collection === doc.collection && d.id === doc.id);
469
- if (i < 0)
470
- next.push(doc);
471
- else
472
- next[i] = doc;
473
- }
474
- return next;
475
- });
415
+ // Replace-or-append (upsert): an imported row may be brand new to the
416
+ // session, so each accepted document must land even with no prior
417
+ // entry for its ref.
418
+ setDocuments((prev) => accepted.reduce((acc, doc) => upsertDoc(acc, doc), prev));
476
419
  for (const doc of accepted)
477
420
  persisterRef.current?.upsertDocument(doc);
478
421
  // EC-224: imports land as the working DRAFT. Publish-ness is the version
@@ -556,7 +499,7 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
556
499
  // set-field: upsert the document carrying the new value. The write
557
500
  // path self-types it through the schema lens (EC-277); a scalar field
558
501
  // stores its scalar as-is.
559
- const current = documentsRef.current.find((d) => d.collection === ref.collection && d.id === ref.id);
502
+ const current = documentsRef.current.find((d) => matchesRef(d, ref));
560
503
  if (!current) {
561
504
  return Promise.resolve({
562
505
  ok: false,
@@ -590,10 +533,12 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
590
533
  // `state` mirror to keep in sync (published-ness is derived from the
591
534
  // pointer set).
592
535
  applyState: (ref, state) => setPublished(ref, state === 'published'),
593
- applyDelete: (ref) => setDocuments((prev) => prev.filter((d) => !(d.collection === ref.collection && d.id === ref.id))),
536
+ applyDelete: (ref) => setDocuments((prev) => removeDoc(prev, ref)),
594
537
  // EC-247: fold the new value into the session document (same
595
- // locale-aware path the dispatched upsert used).
596
- applySetField: (ref) => setDocuments((prev) => prev.map((d) => d.collection === ref.collection && d.id === ref.id ? setFieldDoc(d) : d)),
538
+ // locale-aware path the dispatched upsert used). Not a plain
539
+ // `replaceDoc` — the replacement is computed FROM each matched
540
+ // document (`setFieldDoc(d)`), not a fixed value to swap in.
541
+ applySetField: (ref) => setDocuments((prev) => prev.map((d) => (matchesRef(d, ref) ? setFieldDoc(d) : d))),
597
542
  ...(dispatch ? { dispatch } : {}),
598
543
  }, collectionId, ids, action, setField);
599
544
  }, [projectId, titleOf, registry, localeConfig, activeLocale, setPublished, assetExists]);
@@ -682,29 +627,6 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
682
627
  ]);
683
628
  return _jsx(CmsContext.Provider, { value: store$, children: children });
684
629
  }
685
- export function useCms() {
686
- const store = useContext(CmsContext);
687
- if (!store)
688
- throw new Error('useCms must be used within a <CmsProvider>.');
689
- return store;
690
- }
691
- /**
692
- * A field's display label (EC-241): resolve its (possibly localized) `form.label`,
693
- * falling back to the field name. **`locale` defaults to the active UI locale** —
694
- * a bare `fieldLabel(field)` localizes a `{ en, de }` map against the browser locale
695
- * (via the non-hook `localizedLabel`), so a German editor sees German labels. Pass an
696
- * explicit `locale` only to force a specific one (e.g. a content-locale variant view).
697
- */
698
- export function fieldLabel(field, locale) {
699
- // A catalog-backed built-in-rail label (msgLabel) always resolves through the
700
- // catalog against the active UI locale, ignoring a content-locale override — a rail
701
- // is chrome, not localized content.
702
- const chrome = resolveMsgLabel(field.form?.label);
703
- if (chrome !== undefined)
704
- return chrome;
705
- if (locale !== undefined)
706
- return resolveLabel(field.form?.label, locale) ?? field.name;
707
- return localizedLabel(field.form?.label, field.name);
708
- }
630
+ export { useCms };
709
631
  export { primaryField };
710
632
  //# sourceMappingURL=cms-store.js.map