@elytracms/studio 0.0.42 → 0.0.43

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 (141) hide show
  1. package/dist/components/assets/asset-detail.js +1 -1
  2. package/dist/components/assets/asset-detail.js.map +1 -1
  3. package/dist/components/assets/assets-module.js +19 -6
  4. package/dist/components/assets/assets-module.js.map +1 -1
  5. package/dist/components/assets/assets-store.d.ts +2 -1
  6. package/dist/components/assets/assets-store.js +4 -8
  7. package/dist/components/assets/assets-store.js.map +1 -1
  8. package/dist/components/assets/assets-view-preference.d.ts +0 -8
  9. package/dist/components/assets/assets-view-preference.js +12 -13
  10. package/dist/components/assets/assets-view-preference.js.map +1 -1
  11. package/dist/components/auth-context.d.ts +2 -1
  12. package/dist/components/auth-context.js +6 -8
  13. package/dist/components/auth-context.js.map +1 -1
  14. package/dist/components/builder/builder-store.d.ts +2 -2
  15. package/dist/components/builder/builder-store.js +4 -8
  16. package/dist/components/builder/builder-store.js.map +1 -1
  17. package/dist/components/cms/bulk-actions.js +2 -1
  18. package/dist/components/cms/bulk-actions.js.map +1 -1
  19. package/dist/components/cms/cell-edit.js +2 -1
  20. package/dist/components/cms/cell-edit.js.map +1 -1
  21. package/dist/components/cms/cms-composition-validators.d.ts +25 -0
  22. package/dist/components/cms/cms-composition-validators.js +75 -0
  23. package/dist/components/cms/cms-composition-validators.js.map +1 -0
  24. package/dist/components/cms/cms-labels.d.ts +15 -0
  25. package/dist/components/cms/cms-labels.js +27 -0
  26. package/dist/components/cms/cms-labels.js.map +1 -0
  27. package/dist/components/cms/cms-store.d.ts +23 -10
  28. package/dist/components/cms/cms-store.js +78 -154
  29. package/dist/components/cms/cms-store.js.map +1 -1
  30. package/dist/components/cms/collection-table.js +1 -1
  31. package/dist/components/cms/collection-table.js.map +1 -1
  32. package/dist/components/cms/detail-editor.js +33 -42
  33. package/dist/components/cms/detail-editor.js.map +1 -1
  34. package/dist/components/cms/discard-draft.d.ts +18 -0
  35. package/dist/components/cms/discard-draft.js +18 -0
  36. package/dist/components/cms/discard-draft.js.map +1 -1
  37. package/dist/components/cms/document-editor-context.d.ts +2 -1
  38. package/dist/components/cms/document-editor-context.js +4 -9
  39. package/dist/components/cms/document-editor-context.js.map +1 -1
  40. package/dist/components/cms/document-ref-utils.d.ts +33 -0
  41. package/dist/components/cms/document-ref-utils.js +47 -0
  42. package/dist/components/cms/document-ref-utils.js.map +1 -0
  43. package/dist/components/cms/field-control.js +1 -1
  44. package/dist/components/cms/field-control.js.map +1 -1
  45. package/dist/components/cms/issues-pane.js +1 -1
  46. package/dist/components/cms/issues-pane.js.map +1 -1
  47. package/dist/components/cms/rich-text/block-commands.d.ts +36 -0
  48. package/dist/components/cms/rich-text/block-commands.js +47 -0
  49. package/dist/components/cms/rich-text/block-commands.js.map +1 -0
  50. package/dist/components/cms/rich-text/slash-command.js +25 -8
  51. package/dist/components/cms/rich-text/slash-command.js.map +1 -1
  52. package/dist/components/cms/rich-text/toolbar.js +3 -2
  53. package/dist/components/cms/rich-text/toolbar.js.map +1 -1
  54. package/dist/components/cms/version-compare.js +1 -1
  55. package/dist/components/cms/version-compare.js.map +1 -1
  56. package/dist/components/cms/versions-model.d.ts +31 -0
  57. package/dist/components/cms/versions-model.js +30 -0
  58. package/dist/components/cms/versions-model.js.map +1 -1
  59. package/dist/components/cms/view-bar.js +9 -6
  60. package/dist/components/cms/view-bar.js.map +1 -1
  61. package/dist/components/collab/collab-context.d.ts +2 -1
  62. package/dist/components/collab/collab-context.js +4 -8
  63. package/dist/components/collab/collab-context.js.map +1 -1
  64. package/dist/components/collection-view/collection-view-context.d.ts +9 -1
  65. package/dist/components/collection-view/collection-view-context.js +4 -9
  66. package/dist/components/collection-view/collection-view-context.js.map +1 -1
  67. package/dist/components/components/component-detail.js +2 -5
  68. package/dist/components/components/component-detail.js.map +1 -1
  69. package/dist/components/components/component-preview.js +8 -4
  70. package/dist/components/components/component-preview.js.map +1 -1
  71. package/dist/components/components/registry-browser.js +24 -5
  72. package/dist/components/components/registry-browser.js.map +1 -1
  73. package/dist/components/components/transfer-dialog.d.ts +10 -0
  74. package/dist/components/components/transfer-dialog.js +24 -8
  75. package/dist/components/components/transfer-dialog.js.map +1 -1
  76. package/dist/components/delivery-health-banner.d.ts +12 -6
  77. package/dist/components/delivery-health-banner.js +27 -29
  78. package/dist/components/delivery-health-banner.js.map +1 -1
  79. package/dist/components/dev-login-badge.d.ts +0 -2
  80. package/dist/components/dev-login-badge.js +1 -3
  81. package/dist/components/dev-login-badge.js.map +1 -1
  82. package/dist/components/playground-badge.d.ts +0 -2
  83. package/dist/components/playground-badge.js +0 -2
  84. package/dist/components/playground-badge.js.map +1 -1
  85. package/dist/components/project-list.js +2 -6
  86. package/dist/components/project-list.js.map +1 -1
  87. package/dist/components/project-session.d.ts +6 -2
  88. package/dist/components/project-session.js +32 -83
  89. package/dist/components/project-session.js.map +1 -1
  90. package/dist/components/project-switcher.js +2 -6
  91. package/dist/components/project-switcher.js.map +1 -1
  92. package/dist/components/redirects/redirects-store.d.ts +2 -1
  93. package/dist/components/redirects/redirects-store.js +4 -8
  94. package/dist/components/redirects/redirects-store.js.map +1 -1
  95. package/dist/components/studio-context.d.ts +2 -1
  96. package/dist/components/studio-context.js +4 -8
  97. package/dist/components/studio-context.js.map +1 -1
  98. package/dist/components/studio-shell.js +1 -1
  99. package/dist/components/studio-shell.js.map +1 -1
  100. package/dist/components/theme-context.d.ts +2 -1
  101. package/dist/components/theme-context.js +4 -8
  102. package/dist/components/theme-context.js.map +1 -1
  103. package/dist/components/use-hydration-phase.d.ts +65 -0
  104. package/dist/components/use-hydration-phase.js +64 -0
  105. package/dist/components/use-hydration-phase.js.map +1 -0
  106. package/dist/components/users/users-store.d.ts +2 -1
  107. package/dist/components/users/users-store.js +4 -8
  108. package/dist/components/users/users-store.js.map +1 -1
  109. package/dist/components/workspace-projects.d.ts +14 -0
  110. package/dist/components/workspace-projects.js +21 -0
  111. package/dist/components/workspace-projects.js.map +1 -1
  112. package/dist/lib/data/data-store.d.ts +2 -1
  113. package/dist/lib/data/data-store.js +4 -8
  114. package/dist/lib/data/data-store.js.map +1 -1
  115. package/dist/lib/delivery/instant-publish.d.ts +9 -13
  116. package/dist/lib/delivery/instant-publish.js +0 -7
  117. package/dist/lib/delivery/instant-publish.js.map +1 -1
  118. package/dist/lib/diff-view-preference.js +4 -4
  119. package/dist/lib/diff-view-preference.js.map +1 -1
  120. package/dist/lib/i18n/messages.en.js +1 -0
  121. package/dist/lib/i18n/messages.en.js.map +1 -1
  122. package/dist/lib/local-preference.d.ts +37 -0
  123. package/dist/lib/local-preference.js +36 -0
  124. package/dist/lib/local-preference.js.map +1 -1
  125. package/dist/lib/persistence/convex-adapter.d.ts +3 -1
  126. package/dist/lib/persistence/convex-adapter.js +20 -0
  127. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  128. package/dist/lib/rail.js +7 -6
  129. package/dist/lib/rail.js.map +1 -1
  130. package/dist/lib/required-context.d.ts +24 -0
  131. package/dist/lib/required-context.js +28 -0
  132. package/dist/lib/required-context.js.map +1 -0
  133. package/dist/lib/theme.js +11 -4
  134. package/dist/lib/theme.js.map +1 -1
  135. package/dist/ui/index.d.ts +1 -0
  136. package/dist/ui/index.js +1 -0
  137. package/dist/ui/index.js.map +1 -1
  138. package/dist/ui/segmented-control.d.ts +46 -0
  139. package/dist/ui/segmented-control.js +13 -0
  140. package/dist/ui/segmented-control.js.map +1 -0
  141. package/package.json +2 -2
@@ -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
14
  import { stagedDocuments } 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 {
@@ -209,9 +168,9 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
209
168
  configureStudioLocales(localeConfig);
210
169
  }, [localeConfig]);
211
170
  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]);
171
+ const getDocument = useCallback((ref) => documents.find((d) => matchesRef(d, ref)), [documents]);
213
172
  const titleOf = useCallback((ref) => {
214
- const doc = documents.find((d) => d.collection === ref.collection && d.id === ref.id);
173
+ const doc = documents.find((d) => matchesRef(d, ref));
215
174
  const collection = registry.get(ref.collection);
216
175
  if (!doc || !collection)
217
176
  return ref.id;
@@ -232,18 +191,15 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
232
191
  return typeof value === 'string' && value ? value : ref.id;
233
192
  }, [documents, registry, localeConfig, activeLocale]);
234
193
  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
- });
194
+ // Replace-or-append (upsert): a brand-new document must still land even
195
+ // though nothing in the session matches its ref yet.
196
+ setDocuments((prev) => upsertDoc(prev, doc));
243
197
  persisterRef.current?.upsertDocument(doc);
244
198
  }, []);
245
199
  const commitFieldEdit = useCallback((input) => {
246
- const replaceWith = (doc) => setDocuments((prev) => prev.map((d) => (d.collection === doc.collection && d.id === doc.id ? doc : d)));
200
+ // Pure map-replace: an edit always targets a document already open in
201
+ // the session, so a miss (already gone) is a no-op, never an append.
202
+ const replaceWith = (doc) => setDocuments((prev) => replaceDoc(prev, doc));
247
203
  const persister = persisterRef.current;
248
204
  return commitCellEdit({
249
205
  registry,
@@ -264,22 +220,24 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
264
220
  : {}),
265
221
  }, input);
266
222
  }, [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]);
223
+ // Shared insert sequence (EC-326): existingIds/mint/seedDefaults (via
224
+ // `buildNewDocument`) + optimistic append + persist, consumed by both
225
+ // `createDocument` (always inserts) and `ensureSingletonDocument`'s empty
226
+ // branch (`guardExisting: true` — re-checks "no document for this collection
227
+ // yet" against the LATEST `prev` inside the `setDocuments` updater, not the
228
+ // `documentsRef.current` snapshot the caller already checked, so a
229
+ // concurrent invocation — StrictMode's double-invoked effect — can never
230
+ // materialize a second singleton document).
231
+ const insertNewDocument = useCallback((collectionId, options) => {
232
+ const doc = buildNewDocument(collectionId, registry, seq, documentsRef.current);
233
+ setDocuments((prev) => options?.guardExisting && hasDocumentFor(prev, collectionId) ? prev : [...prev, doc]);
280
234
  persisterRef.current?.upsertDocument(doc);
281
- return { collection: collectionId, id };
235
+ return doc;
282
236
  }, [registry]);
237
+ const createDocument = useCallback((collectionId) => {
238
+ const doc = insertNewDocument(collectionId);
239
+ return { collection: collectionId, id: doc.id };
240
+ }, [insertNewDocument]);
283
241
  // EC-274 (re-scopes EC-217): a singleton is a UI lens, not a fixed id. Resolve
284
242
  // the collection's lone document and return its ref; if none exists yet, create
285
243
  // one with a NORMAL generated id (never the collection id — no code ever names a
@@ -308,22 +266,14 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
308
266
  return { collection: collectionId, id: `new-${collectionId}-pending` };
309
267
  }
310
268
  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]);
269
+ // EC-316: same collision-free mint as `createDocument` (via the shared
270
+ // `insertNewDocument` — defensive here, the singleton-empty branch only
271
+ // runs when no document exists yet).
272
+ const doc = insertNewDocument(collectionId, { guardExisting: true });
273
+ return { collection: collectionId, id: doc.id };
274
+ }, [registry, insertNewDocument]);
325
275
  const deleteDocument = useCallback((ref) => {
326
- setDocuments((prev) => prev.filter((d) => !(d.collection === ref.collection && d.id === ref.id)));
276
+ setDocuments((prev) => removeDoc(prev, ref));
327
277
  persisterRef.current?.deleteDocument(ref);
328
278
  }, []);
329
279
  // EC-224: published-ness IS the version pointer (`publishedKeys`) — the SOLE
@@ -412,7 +362,9 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
412
362
  return null;
413
363
  }
414
364
  const restored = result.value.document;
415
- setDocuments((prev) => prev.map((d) => d.collection === restored.collection && d.id === restored.id ? restored : d));
365
+ // Pure map-replace: a restore always targets a document already open in
366
+ // the session.
367
+ setDocuments((prev) => replaceDoc(prev, restored));
416
368
  return restored;
417
369
  }, [projectId]);
418
370
  const validate = useCallback((doc) => validateDocument(registry, doc, {
@@ -462,17 +414,10 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
462
414
  accepted.push(doc);
463
415
  }
464
416
  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
- });
417
+ // Replace-or-append (upsert): an imported row may be brand new to the
418
+ // session, so each accepted document must land even with no prior
419
+ // entry for its ref.
420
+ setDocuments((prev) => accepted.reduce((acc, doc) => upsertDoc(acc, doc), prev));
476
421
  for (const doc of accepted)
477
422
  persisterRef.current?.upsertDocument(doc);
478
423
  // EC-224: imports land as the working DRAFT. Publish-ness is the version
@@ -556,7 +501,7 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
556
501
  // set-field: upsert the document carrying the new value. The write
557
502
  // path self-types it through the schema lens (EC-277); a scalar field
558
503
  // stores its scalar as-is.
559
- const current = documentsRef.current.find((d) => d.collection === ref.collection && d.id === ref.id);
504
+ const current = documentsRef.current.find((d) => matchesRef(d, ref));
560
505
  if (!current) {
561
506
  return Promise.resolve({
562
507
  ok: false,
@@ -590,10 +535,12 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
590
535
  // `state` mirror to keep in sync (published-ness is derived from the
591
536
  // pointer set).
592
537
  applyState: (ref, state) => setPublished(ref, state === 'published'),
593
- applyDelete: (ref) => setDocuments((prev) => prev.filter((d) => !(d.collection === ref.collection && d.id === ref.id))),
538
+ applyDelete: (ref) => setDocuments((prev) => removeDoc(prev, ref)),
594
539
  // 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)),
540
+ // locale-aware path the dispatched upsert used). Not a plain
541
+ // `replaceDoc` — the replacement is computed FROM each matched
542
+ // document (`setFieldDoc(d)`), not a fixed value to swap in.
543
+ applySetField: (ref) => setDocuments((prev) => prev.map((d) => (matchesRef(d, ref) ? setFieldDoc(d) : d))),
597
544
  ...(dispatch ? { dispatch } : {}),
598
545
  }, collectionId, ids, action, setField);
599
546
  }, [projectId, titleOf, registry, localeConfig, activeLocale, setPublished, assetExists]);
@@ -682,29 +629,6 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
682
629
  ]);
683
630
  return _jsx(CmsContext.Provider, { value: store$, children: children });
684
631
  }
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
- }
632
+ export { useCms };
709
633
  export { primaryField };
710
634
  //# sourceMappingURL=cms-store.js.map