@elytracms/studio 0.0.80 → 0.0.82

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 (196) hide show
  1. package/convex/contract-parity.test.ts +1 -1
  2. package/convex/functions.ts +13 -3
  3. package/convex/http-routes.test.ts +143 -0
  4. package/convex/http.ts +19 -9
  5. package/convex/redirect-revalidate.test.ts +130 -0
  6. package/convex/revalidate.ts +9 -7
  7. package/convex/schema.ts +0 -19
  8. package/convex/triggers.ts +33 -3
  9. package/convex/users.ts +1 -1
  10. package/dist/components/ai/ai-chat-panel.js +26 -4
  11. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  12. package/dist/components/ai/proposal-card.d.ts +27 -0
  13. package/dist/components/ai/proposal-card.js +31 -3
  14. package/dist/components/ai/proposal-card.js.map +1 -1
  15. package/dist/components/ai/proposal-issues.d.ts +55 -0
  16. package/dist/components/ai/proposal-issues.js +69 -0
  17. package/dist/components/ai/proposal-issues.js.map +1 -0
  18. package/dist/components/ai/translate-document-dialog.js +38 -3
  19. package/dist/components/ai/translate-document-dialog.js.map +1 -1
  20. package/dist/components/app-views/default-modules.d.ts +141 -44
  21. package/dist/components/app-views/default-modules.js +229 -65
  22. package/dist/components/app-views/default-modules.js.map +1 -1
  23. package/dist/components/app-views/module.d.ts +38 -0
  24. package/dist/components/app-views/module.js.map +1 -1
  25. package/dist/components/app-views/sidebar.d.ts +5 -2
  26. package/dist/components/app-views/sidebar.js +16 -5
  27. package/dist/components/app-views/sidebar.js.map +1 -1
  28. package/dist/components/app-views/studio-config.d.ts +28 -1
  29. package/dist/components/app-views/studio-config.js +1 -1
  30. package/dist/components/app-views/studio-config.js.map +1 -1
  31. package/dist/components/builder/canvas-render-context.js +10 -2
  32. package/dist/components/builder/canvas-render-context.js.map +1 -1
  33. package/dist/components/builder/clipboard-actions.js +5 -3
  34. package/dist/components/builder/clipboard-actions.js.map +1 -1
  35. package/dist/components/builder/reuse-context.js +7 -1
  36. package/dist/components/builder/reuse-context.js.map +1 -1
  37. package/dist/components/builder/reuse.d.ts +94 -16
  38. package/dist/components/builder/reuse.js +201 -19
  39. package/dist/components/builder/reuse.js.map +1 -1
  40. package/dist/components/cms/bulk-actions.js +10 -5
  41. package/dist/components/cms/bulk-actions.js.map +1 -1
  42. package/dist/components/cms/cell-edit.d.ts +18 -3
  43. package/dist/components/cms/cell-edit.js +29 -7
  44. package/dist/components/cms/cell-edit.js.map +1 -1
  45. package/dist/components/cms/cms-store.d.ts +54 -14
  46. package/dist/components/cms/cms-store.js +142 -45
  47. package/dist/components/cms/cms-store.js.map +1 -1
  48. package/dist/components/cms/collection-table.js +2 -2
  49. package/dist/components/cms/collection-table.js.map +1 -1
  50. package/dist/components/cms/detail-editor.js +15 -14
  51. package/dist/components/cms/detail-editor.js.map +1 -1
  52. package/dist/components/cms/document-editor-context.d.ts +12 -1
  53. package/dist/components/cms/document-editor-context.js.map +1 -1
  54. package/dist/components/cms/field-control.js +22 -8
  55. package/dist/components/cms/field-control.js.map +1 -1
  56. package/dist/components/cms/field-renderer.js +8 -1
  57. package/dist/components/cms/field-renderer.js.map +1 -1
  58. package/dist/components/cms/gate-validation.d.ts +36 -0
  59. package/dist/components/cms/gate-validation.js +17 -0
  60. package/dist/components/cms/gate-validation.js.map +1 -0
  61. package/dist/components/cms/import-export.js +2 -1
  62. package/dist/components/cms/import-export.js.map +1 -1
  63. package/dist/components/cms/inline-field-errors.d.ts +8 -0
  64. package/dist/components/cms/inline-field-errors.js +52 -0
  65. package/dist/components/cms/inline-field-errors.js.map +1 -0
  66. package/dist/components/cms/issues-pane.d.ts +4 -2
  67. package/dist/components/cms/issues-pane.js +13 -7
  68. package/dist/components/cms/issues-pane.js.map +1 -1
  69. package/dist/components/cms/slug-collision-report.d.ts +41 -0
  70. package/dist/components/cms/slug-collision-report.js +52 -0
  71. package/dist/components/cms/slug-collision-report.js.map +1 -0
  72. package/dist/components/components/transfer-dialog.js +10 -12
  73. package/dist/components/components/transfer-dialog.js.map +1 -1
  74. package/dist/components/deployment-setup-gate.d.ts +24 -0
  75. package/dist/components/deployment-setup-gate.js +29 -4
  76. package/dist/components/deployment-setup-gate.js.map +1 -1
  77. package/dist/components/issue-severity.d.ts +73 -0
  78. package/dist/components/issue-severity.js +56 -0
  79. package/dist/components/issue-severity.js.map +1 -0
  80. package/dist/components/nav-issues.d.ts +68 -0
  81. package/dist/components/nav-issues.js +78 -0
  82. package/dist/components/nav-issues.js.map +1 -0
  83. package/dist/components/operation-error.js +3 -3
  84. package/dist/components/operation-error.js.map +1 -1
  85. package/dist/components/project-list.d.ts +9 -3
  86. package/dist/components/project-list.js +11 -5
  87. package/dist/components/project-list.js.map +1 -1
  88. package/dist/components/project-switcher.d.ts +3 -1
  89. package/dist/components/project-switcher.js +4 -2
  90. package/dist/components/project-switcher.js.map +1 -1
  91. package/dist/components/redirects/redirects-store.d.ts +21 -0
  92. package/dist/components/redirects/redirects-store.js +17 -4
  93. package/dist/components/redirects/redirects-store.js.map +1 -1
  94. package/dist/components/section-nav.js +75 -10
  95. package/dist/components/section-nav.js.map +1 -1
  96. package/dist/customization.d.ts +7 -0
  97. package/dist/customization.js.map +1 -1
  98. package/dist/lib/ai/apply-content.js +6 -7
  99. package/dist/lib/ai/apply-content.js.map +1 -1
  100. package/dist/lib/ai/chat-context.d.ts +32 -26
  101. package/dist/lib/ai/chat-context.js +81 -9
  102. package/dist/lib/ai/chat-context.js.map +1 -1
  103. package/dist/lib/ai/constraints.d.ts +42 -0
  104. package/dist/lib/ai/constraints.js +259 -0
  105. package/dist/lib/ai/constraints.js.map +1 -0
  106. package/dist/lib/ai/index.d.ts +2 -0
  107. package/dist/lib/ai/index.js +2 -0
  108. package/dist/lib/ai/index.js.map +1 -1
  109. package/dist/lib/ai/proposal-validation.d.ts +26 -0
  110. package/dist/lib/ai/proposal-validation.js +17 -0
  111. package/dist/lib/ai/proposal-validation.js.map +1 -0
  112. package/dist/lib/ai/shape.js +60 -13
  113. package/dist/lib/ai/shape.js.map +1 -1
  114. package/dist/lib/ai/tools.d.ts +8 -4
  115. package/dist/lib/ai/tools.js +43 -14
  116. package/dist/lib/ai/tools.js.map +1 -1
  117. package/dist/lib/ai/translate-document.d.ts +35 -1
  118. package/dist/lib/ai/translate-document.js +16 -1
  119. package/dist/lib/ai/translate-document.js.map +1 -1
  120. package/dist/lib/ai/types.d.ts +11 -6
  121. package/dist/lib/ai/types.js.map +1 -1
  122. package/dist/lib/ai/update-fields.d.ts +8 -6
  123. package/dist/lib/ai/update-fields.js +9 -22
  124. package/dist/lib/ai/update-fields.js.map +1 -1
  125. package/dist/lib/clipboard/paste-issues.d.ts +5 -0
  126. package/dist/lib/clipboard/paste-issues.js +16 -6
  127. package/dist/lib/clipboard/paste-issues.js.map +1 -1
  128. package/dist/lib/cms/document-gate.d.ts +37 -0
  129. package/dist/lib/cms/document-gate.js +40 -0
  130. package/dist/lib/cms/document-gate.js.map +1 -0
  131. package/dist/lib/cms-views/view-model.d.ts +5 -0
  132. package/dist/lib/cms-views/view-model.js +10 -4
  133. package/dist/lib/cms-views/view-model.js.map +1 -1
  134. package/dist/lib/data/collection-content.d.ts +6 -1
  135. package/dist/lib/data/collection-content.js +51 -15
  136. package/dist/lib/data/collection-content.js.map +1 -1
  137. package/dist/lib/i18n/i18n.d.ts +51 -16
  138. package/dist/lib/i18n/i18n.js +119 -43
  139. package/dist/lib/i18n/i18n.js.map +1 -1
  140. package/dist/lib/i18n/index.d.ts +8 -1
  141. package/dist/lib/i18n/index.js +1 -1
  142. package/dist/lib/i18n/index.js.map +1 -1
  143. package/dist/lib/i18n/messages.de.js +200 -0
  144. package/dist/lib/i18n/messages.de.js.map +1 -1
  145. package/dist/lib/i18n/messages.en.d.ts +162 -0
  146. package/dist/lib/i18n/messages.en.js +226 -0
  147. package/dist/lib/i18n/messages.en.js.map +1 -1
  148. package/dist/lib/i18n/types.d.ts +15 -9
  149. package/dist/lib/i18n/types.js.map +1 -1
  150. package/dist/lib/locale/binding-fallback.d.ts +8 -1
  151. package/dist/lib/locale/binding-fallback.js +8 -1
  152. package/dist/lib/locale/binding-fallback.js.map +1 -1
  153. package/dist/lib/locale/locale-store.d.ts +41 -9
  154. package/dist/lib/locale/locale-store.js +34 -15
  155. package/dist/lib/locale/locale-store.js.map +1 -1
  156. package/dist/lib/locale/locale-switcher.d.ts +3 -1
  157. package/dist/lib/locale/locale-switcher.js +4 -2
  158. package/dist/lib/locale/locale-switcher.js.map +1 -1
  159. package/dist/lib/locale/use-studio-locale.d.ts +9 -4
  160. package/dist/lib/locale/use-studio-locale.js.map +1 -1
  161. package/dist/lib/operations/studio-operations.js +19 -2
  162. package/dist/lib/operations/studio-operations.js.map +1 -1
  163. package/dist/lib/persistence/convex-adapter.d.ts +1 -3
  164. package/dist/lib/persistence/convex-adapter.js +0 -55
  165. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  166. package/dist/lib/persistence/deployment-readiness.js +10 -0
  167. package/dist/lib/persistence/deployment-readiness.js.map +1 -1
  168. package/dist/lib/persona/labels.d.ts +85 -9
  169. package/dist/lib/persona/labels.js +198 -58
  170. package/dist/lib/persona/labels.js.map +1 -1
  171. package/dist/lib/routes/route-issues.d.ts +9 -0
  172. package/dist/lib/routes/route-issues.js +117 -0
  173. package/dist/lib/routes/route-issues.js.map +1 -0
  174. package/dist/lib/webhooks/dispatcher.d.ts +5 -2
  175. package/dist/lib/webhooks/dispatcher.js.map +1 -1
  176. package/dist/lib/webhooks/tags.js +150 -17
  177. package/dist/lib/webhooks/tags.js.map +1 -1
  178. package/dist/lib/workspace/config-issues.d.ts +56 -0
  179. package/dist/lib/workspace/config-issues.js +72 -0
  180. package/dist/lib/workspace/config-issues.js.map +1 -0
  181. package/dist/lib/workspace/index.d.ts +24 -8
  182. package/dist/lib/workspace/index.js +28 -18
  183. package/dist/lib/workspace/index.js.map +1 -1
  184. package/dist/lib/workspace/switcher-source.d.ts +20 -3
  185. package/dist/lib/workspace/switcher-source.js.map +1 -1
  186. package/dist/lib/workspace/workspace-source.js +27 -0
  187. package/dist/lib/workspace/workspace-source.js.map +1 -1
  188. package/dist/previews/manifest-format.d.ts +13 -6
  189. package/dist/previews/manifest-format.js +9 -2
  190. package/dist/previews/manifest-format.js.map +1 -1
  191. package/dist/router/root.d.ts +36 -1
  192. package/dist/router/root.js +54 -7
  193. package/dist/router/root.js.map +1 -1
  194. package/package.json +2 -2
  195. package/convex/cliTokens.ts +0 -63
  196. package/convex/sync.ts +0 -297
@@ -32,16 +32,22 @@ export interface StudioI18nConfig {
32
32
  */
33
33
  defaultLocale?: string;
34
34
  /**
35
- * **Pin** the studio UI to a fixed locale, overriding browser detection. Set this
36
- * for a single-language deployment (e.g. a German site whose editors work in German).
35
+ * **Pin** the studio UI to a fixed locale, overriding browser detection. **This is the
36
+ * supported way to choose the studio's language** (EC-513) — set it for any deployment
37
+ * whose editors work in one language, which is what both production consumers do.
37
38
  *
38
- * Why it exists: browser detection reads `navigator.languages`, which is **client-only**
39
- * — under SSR the server can't see it, so it renders the default locale while the client
40
- * renders the browser locale → a hydration mismatch (server text ≠ client text). A pin is
41
- * read from *config*, which is imported identically on server and client, so both render
42
- * the same locale with no header plumbing and no first-paint flash. When a locale is not
43
- * available (not `defaultLocale` and not in `locales`) the pin is ignored (with a console
44
- * signal) and detection resumes. Omit it to keep browser-preferred detection.
39
+ * Why prefer it: browser detection reads `navigator.languages`, which is **client-only**.
40
+ * The server renders the default locale, and the studio therefore boots in that locale
41
+ * and hands over to the browser's preference only after hydration — correct, but it
42
+ * means a moment of default-locale chrome on a cold load. A pin is read from *config*,
43
+ * imported identically on server and client, so the right language is there from the
44
+ * first byte.
45
+ *
46
+ * There is deliberately **no server-side locale seam** (no `Accept-Language` resolver):
47
+ * locale resolution is client-side, and a pin is how a host states the answer up front.
48
+ *
49
+ * A locale that is not available (neither `defaultLocale` nor a key of `locales`) is
50
+ * ignored with a console signal, and detection resumes. Omit it to follow the browser.
45
51
  */
46
52
  locale?: string;
47
53
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/lib/i18n/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAgDH;;;;GAIG;AACH,MAAM,UAAU,GAAG,CAAC,OAAe,EAAE,WAAoB;IACvD,OAAO,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAA;AAC3E,CAAC","sourcesContent":["/**\n * Studio-UI localization types (EC-241) — the leaf module both the message catalog\n * and the resolver import, so nothing here may import studio runtime code (keeps the\n * `defineStudio` config surface free of import cycles).\n *\n * This is the **studio-chrome** axis (engine-owned strings). Schema labels are a\n * separate mechanism — a {@link import('@elytracms/core/cms-core').LocalizedString}\n * on the field/collection def, resolved via `resolveLabel`. Content-*value*\n * localization is a third, unrelated axis (`cms-core/localization.ts`).\n */\n\n/** A source-catalog entry: the English string + an optional translator hint. */\nexport interface SourceMessage {\n /** The English (source-of-truth) string. */\n message: string\n /**\n * Translator hint/context — the same word can translate differently by context\n * (the standard `msgctxt`/Lingui-comment idea). **Authoring/translation-time only:**\n * it lives on the in-engine source catalog and is NOT shipped in the flat\n * consumer-provided translation catalogs.\n */\n description?: string\n}\n\n/** The in-engine English source catalog: message key → {@link SourceMessage}. */\nexport type SourceCatalog = Record<string, SourceMessage>\n\n/** A consumer-provided translation catalog: flat message key → localized string. */\nexport type MessageCatalog = Record<string, string>\n\n/** The studio-UI localization config passed to `defineStudio({ i18n })` (EC-241). */\nexport interface StudioI18nConfig {\n /**\n * The locale used when the browser's preferred locale matches nothing available.\n * Defaults to `'en'` (the in-engine source language).\n */\n defaultLocale?: string\n /**\n * **Pin** the studio UI to a fixed locale, overriding browser detection. Set this\n * for a single-language deployment (e.g. a German site whose editors work in German).\n *\n * Why it exists: browser detection reads `navigator.languages`, which is **client-only**\n * — under SSR the server can't see it, so it renders the default locale while the client\n * renders the browser locale → a hydration mismatch (server text ≠ client text). A pin is\n * read from *config*, which is imported identically on server and client, so both render\n * the same locale with no header plumbing and no first-paint flash. When a locale is not\n * available (not `defaultLocale` and not in `locales`) the pin is ignored (with a console\n * signal) and detection resumes. Omit it to keep browser-preferred detection.\n */\n locale?: string\n /**\n * Imported translation catalogs keyed by locale code, e.g. `{ de: deMessages }`.\n * English is always available as the built-in source, so it need not be listed.\n */\n locales?: Record<string, MessageCatalog>\n}\n\n/**\n * Author a source-catalog entry with an optional translator hint (EC-241). Kept in\n * this leaf module so `messages.en.ts` can build the catalog without importing the\n * resolver (which would import studio runtime code).\n */\nexport function msg(message: string, description?: string): SourceMessage {\n return description !== undefined ? { message, description } : { message }\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/lib/i18n/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAsDH;;;;GAIG;AACH,MAAM,UAAU,GAAG,CAAC,OAAe,EAAE,WAAoB;IACvD,OAAO,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAA;AAC3E,CAAC","sourcesContent":["/**\n * Studio-UI localization types (EC-241) — the leaf module both the message catalog\n * and the resolver import, so nothing here may import studio runtime code (keeps the\n * `defineStudio` config surface free of import cycles).\n *\n * This is the **studio-chrome** axis (engine-owned strings). Schema labels are a\n * separate mechanism — a {@link import('@elytracms/core/cms-core').LocalizedString}\n * on the field/collection def, resolved via `resolveLabel`. Content-*value*\n * localization is a third, unrelated axis (`cms-core/localization.ts`).\n */\n\n/** A source-catalog entry: the English string + an optional translator hint. */\nexport interface SourceMessage {\n /** The English (source-of-truth) string. */\n message: string\n /**\n * Translator hint/context — the same word can translate differently by context\n * (the standard `msgctxt`/Lingui-comment idea). **Authoring/translation-time only:**\n * it lives on the in-engine source catalog and is NOT shipped in the flat\n * consumer-provided translation catalogs.\n */\n description?: string\n}\n\n/** The in-engine English source catalog: message key → {@link SourceMessage}. */\nexport type SourceCatalog = Record<string, SourceMessage>\n\n/** A consumer-provided translation catalog: flat message key → localized string. */\nexport type MessageCatalog = Record<string, string>\n\n/** The studio-UI localization config passed to `defineStudio({ i18n })` (EC-241). */\nexport interface StudioI18nConfig {\n /**\n * The locale used when the browser's preferred locale matches nothing available.\n * Defaults to `'en'` (the in-engine source language).\n */\n defaultLocale?: string\n /**\n * **Pin** the studio UI to a fixed locale, overriding browser detection. **This is the\n * supported way to choose the studio's language** (EC-513) — set it for any deployment\n * whose editors work in one language, which is what both production consumers do.\n *\n * Why prefer it: browser detection reads `navigator.languages`, which is **client-only**.\n * The server renders the default locale, and the studio therefore boots in that locale\n * and hands over to the browser's preference only after hydration — correct, but it\n * means a moment of default-locale chrome on a cold load. A pin is read from *config*,\n * imported identically on server and client, so the right language is there from the\n * first byte.\n *\n * There is deliberately **no server-side locale seam** (no `Accept-Language` resolver):\n * locale resolution is client-side, and a pin is how a host states the answer up front.\n *\n * A locale that is not available (neither `defaultLocale` nor a key of `locales`) is\n * ignored with a console signal, and detection resumes. Omit it to follow the browser.\n */\n locale?: string\n /**\n * Imported translation catalogs keyed by locale code, e.g. `{ de: deMessages }`.\n * English is always available as the built-in source, so it need not be listed.\n */\n locales?: Record<string, MessageCatalog>\n}\n\n/**\n * Author a source-catalog entry with an optional translator hint (EC-241). Kept in\n * this leaf module so `messages.en.ts` can build the catalog without importing the\n * resolver (which would import studio runtime code).\n */\nexport function msg(message: string, description?: string): SourceMessage {\n return description !== undefined ? { message, description } : { message }\n}\n"]}
@@ -25,4 +25,11 @@ export interface BoundValueFallback extends DisplayedFallback {
25
25
  * `null` when the binding is not CMS-shaped, points at no field, or the value
26
26
  * is a real translation.
27
27
  */
28
- export declare function cmsBindingFallback(binding: BindingReference, source: DataSource | undefined, locale: Locale): BoundValueFallback | null;
28
+ export declare function cmsBindingFallback(binding: BindingReference, source: DataSource | undefined,
29
+ /**
30
+ * The locale to judge fallback in. Optional (EC-479): the ambient studio locale
31
+ * is `null` until a project's locale config is installed, and the content engine
32
+ * then resolves in the session's own default — so a caller that has no locale
33
+ * omits it rather than substituting one.
34
+ */
35
+ locale?: Locale): BoundValueFallback | null;
@@ -16,7 +16,14 @@ function pathOfToken(token) {
16
16
  * `null` when the binding is not CMS-shaped, points at no field, or the value
17
17
  * is a real translation.
18
18
  */
19
- export function cmsBindingFallback(binding, source, locale) {
19
+ export function cmsBindingFallback(binding, source,
20
+ /**
21
+ * The locale to judge fallback in. Optional (EC-479): the ambient studio locale
22
+ * is `null` until a project's locale config is installed, and the content engine
23
+ * then resolves in the session's own default — so a caller that has no locale
24
+ * omits it rather than substituting one.
25
+ */
26
+ locale) {
20
27
  if (source === undefined || !isCmsSource(source))
21
28
  return null;
22
29
  if (binding.mode !== 'value' && binding.mode !== 'repeaterItem')
@@ -1 +1 @@
1
- {"version":3,"file":"binding-fallback.js","sourceRoot":"","sources":["../../../src/lib/locale/binding-fallback.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAA;AAEhE,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAA;AAsB1F,SAAS,WAAW,CAAC,MAAkB;IACrC,OAAO,MAAM,CAAC,IAAI,KAAK,KAAK,CAAA;AAC9B,CAAC;AAED,4EAA4E;AAC5E,SAAS,WAAW,CAAC,KAAa;IAChC,OAAO,KAAK;SACT,KAAK,CAAC,GAAG,CAAC;SACV,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;SACvC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAA;AAC1E,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAChC,OAAyB,EACzB,MAA8B,EAC9B,MAAc;IAEd,IAAI,MAAM,KAAK,SAAS,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAA;IAC7D,IAAI,OAAO,CAAC,IAAI,KAAK,OAAO,IAAI,OAAO,CAAC,IAAI,KAAK,cAAc;QAAE,OAAO,IAAI,CAAA;IAE5E,MAAM,MAAM,GAAG,MAAM,CAAC,MAAwB,CAAA;IAC9C,IAAI,IAAI,GAAG,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;IACrC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAElC,wEAAwE;IACxE,4EAA4E;IAC5E,IAAI,UAAU,GAAG,CAAC,CAAA;IAClB,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,KAAK,SAAS,CAAA;IAClD,IAAI,OAAO,CAAC,IAAI,KAAK,OAAO,IAAI,UAAU,EAAE,CAAC;QAC3C,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAE,CAAA;QACrB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAA;QACpC,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,CAAA;QACzB,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IACtB,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAA;IACrB,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAA;IAEpC,0EAA0E;IAC1E,gDAAgD;IAChD,IAAI,UAAU,GAAG,MAAM,CAAC,UAAU,CAAA;IAClC,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAG,sBAAsB,CAAC,MAAM,CAAC,YAAY,EAAE,iBAAiB,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAA;QAC3F,IAAI,CAAC,IAAI,CAAC,EAAE;YAAE,OAAO,IAAI,CAAA;QACzB,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,CAAA;QACxC,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,IAAI,CAAA;QACpC,UAAU,GAAG,KAAK,CAAC,EAAE,CAAA;IACvB,CAAC;IAED,MAAM,EAAE,IAAI,EAAE,GAAG,qBAAqB,CAAC,MAAM,CAAC,YAAY,EAAE,UAAU,EAAE,MAAM,CAAC,CAAA;IAC/E,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,IAAI,CAAA;IAC9B,MAAM,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,CAAA;IAC9E,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,IAAI,CAAA;IACvC,OAAO;QACL,KAAK;QACL,YAAY,EAAE,QAAQ,CAAC,YAAY;QACnC,eAAe,EAAE,QAAQ,CAAC,eAAe;KAC1C,CAAA;AACH,CAAC","sourcesContent":["import type { Locale } from '@elytracms/core/cms-core'\nimport type { BindingReference } from '@elytracms/core/project-graph'\nimport type { CmsSource, DataSource } from '@elytracms/core/data-binding'\nimport { listQueryOfConfig } from '../bindings/section-bindings'\nimport type { QueryCmsConfig } from '../bindings/section-bindings'\nimport { getCollectionDocument, runCollectionListQuery } from '../data/collection-content'\nimport type { DisplayedFallback } from './fallback'\n\n/**\n * Fallback visibility for BOUND content (EC-176): given a stored content\n * binding and its CMS source, decide whether the value the canvas currently\n * previews for the active locale is served via EC-018 locale fallback. Used\n * by the inspector's Content tab — the surface that explains what a bound\n * field shows (the canvas overlay itself is out of this wave's reach; see\n * EC-176 notes).\n *\n * Deterministic over the playground delivery engine: the same\n * `@elytracms/core/content` resolution that produced the previewed value reports\n * which fields fell back (`ResolutionInfo.localeFallbacks`), so the label can\n * never disagree with the displayed value.\n */\n\n/** A previewed bound value that fell back, plus which field it came from. */\nexport interface BoundValueFallback extends DisplayedFallback {\n field: string\n}\n\nfunction isCmsSource(source: DataSource): source is CmsSource {\n return source.kind === 'cms'\n}\n\n/** JSON-pointer-ish token → path segments ('/0/title' → ['0', 'title']). */\nfunction pathOfToken(token: string): string[] {\n return token\n .split('/')\n .filter((segment) => segment.length > 0)\n .map((segment) => segment.replaceAll('~1', '/').replaceAll('~0', '~'))\n}\n\n/**\n * Whether the bound field's previewed value (first entry for list/repeater\n * bindings, the bound document otherwise) is a locale fallback in `locale`.\n * `null` when the binding is not CMS-shaped, points at no field, or the value\n * is a real translation.\n */\nexport function cmsBindingFallback(\n binding: BindingReference,\n source: DataSource | undefined,\n locale: Locale,\n): BoundValueFallback | null {\n if (source === undefined || !isCmsSource(source)) return null\n if (binding.mode !== 'value' && binding.mode !== 'repeaterItem') return null\n\n const config = source.config as QueryCmsConfig\n let path = pathOfToken(binding.token)\n if (path.length === 0) return null\n\n // List-shaped value bindings carry a leading entry index; repeater-item\n // bindings preview the first entry (the same convention as ContentPreview).\n let entryIndex = 0\n const listShaped = config.documentId === undefined\n if (binding.mode === 'value' && listShaped) {\n const head = path[0]!\n if (!/^\\d+$/.test(head)) return null\n entryIndex = Number(head)\n path = path.slice(1)\n }\n const field = path[0]\n if (field === undefined) return null\n\n // Resolve the previewed document's id, then ask the delivery engine which\n // fields fell back for it in the active locale.\n let documentId = config.documentId\n if (documentId === undefined) {\n const list = runCollectionListQuery(config.collectionId, listQueryOfConfig(config), locale)\n if (!list.ok) return null\n const entry = list.documents[entryIndex]\n if (entry === undefined) return null\n documentId = entry.id\n }\n\n const { info } = getCollectionDocument(config.collectionId, documentId, locale)\n if (info === null) return null\n const fellBack = info.localeFallbacks.find((entry) => entry.path[0] === field)\n if (fellBack === undefined) return null\n return {\n field,\n sourceLocale: fellBack.sourceLocale,\n requestedLocale: fellBack.requestedLocale,\n }\n}\n"]}
1
+ {"version":3,"file":"binding-fallback.js","sourceRoot":"","sources":["../../../src/lib/locale/binding-fallback.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Locale } from '@elytracms/core/cms-core'\nimport type { BindingReference } from '@elytracms/core/project-graph'\nimport type { CmsSource, DataSource } from '@elytracms/core/data-binding'\nimport { listQueryOfConfig } from '../bindings/section-bindings'\nimport type { QueryCmsConfig } from '../bindings/section-bindings'\nimport { getCollectionDocument, runCollectionListQuery } from '../data/collection-content'\nimport type { DisplayedFallback } from './fallback'\n\n/**\n * Fallback visibility for BOUND content (EC-176): given a stored content\n * binding and its CMS source, decide whether the value the canvas currently\n * previews for the active locale is served via EC-018 locale fallback. Used\n * by the inspector's Content tab — the surface that explains what a bound\n * field shows (the canvas overlay itself is out of this wave's reach; see\n * EC-176 notes).\n *\n * Deterministic over the playground delivery engine: the same\n * `@elytracms/core/content` resolution that produced the previewed value reports\n * which fields fell back (`ResolutionInfo.localeFallbacks`), so the label can\n * never disagree with the displayed value.\n */\n\n/** A previewed bound value that fell back, plus which field it came from. */\nexport interface BoundValueFallback extends DisplayedFallback {\n field: string\n}\n\nfunction isCmsSource(source: DataSource): source is CmsSource {\n return source.kind === 'cms'\n}\n\n/** JSON-pointer-ish token → path segments ('/0/title' → ['0', 'title']). */\nfunction pathOfToken(token: string): string[] {\n return token\n .split('/')\n .filter((segment) => segment.length > 0)\n .map((segment) => segment.replaceAll('~1', '/').replaceAll('~0', '~'))\n}\n\n/**\n * Whether the bound field's previewed value (first entry for list/repeater\n * bindings, the bound document otherwise) is a locale fallback in `locale`.\n * `null` when the binding is not CMS-shaped, points at no field, or the value\n * is a real translation.\n */\nexport function cmsBindingFallback(\n binding: BindingReference,\n source: DataSource | undefined,\n /**\n * The locale to judge fallback in. Optional (EC-479): the ambient studio locale\n * is `null` until a project's locale config is installed, and the content engine\n * then resolves in the session's own default — so a caller that has no locale\n * omits it rather than substituting one.\n */\n locale?: Locale,\n): BoundValueFallback | null {\n if (source === undefined || !isCmsSource(source)) return null\n if (binding.mode !== 'value' && binding.mode !== 'repeaterItem') return null\n\n const config = source.config as QueryCmsConfig\n let path = pathOfToken(binding.token)\n if (path.length === 0) return null\n\n // List-shaped value bindings carry a leading entry index; repeater-item\n // bindings preview the first entry (the same convention as ContentPreview).\n let entryIndex = 0\n const listShaped = config.documentId === undefined\n if (binding.mode === 'value' && listShaped) {\n const head = path[0]!\n if (!/^\\d+$/.test(head)) return null\n entryIndex = Number(head)\n path = path.slice(1)\n }\n const field = path[0]\n if (field === undefined) return null\n\n // Resolve the previewed document's id, then ask the delivery engine which\n // fields fell back for it in the active locale.\n let documentId = config.documentId\n if (documentId === undefined) {\n const list = runCollectionListQuery(config.collectionId, listQueryOfConfig(config), locale)\n if (!list.ok) return null\n const entry = list.documents[entryIndex]\n if (entry === undefined) return null\n documentId = entry.id\n }\n\n const { info } = getCollectionDocument(config.collectionId, documentId, locale)\n if (info === null) return null\n const fellBack = info.localeFallbacks.find((entry) => entry.path[0] === field)\n if (fellBack === undefined) return null\n return {\n field,\n sourceLocale: fellBack.sourceLocale,\n requestedLocale: fellBack.requestedLocale,\n }\n}\n"]}
@@ -11,20 +11,49 @@ import type { Locale, LocaleConfig } from '@elytracms/core/cms-core';
11
11
  *
12
12
  * Deliberately NOT part of `lib/data/data-store` — the locale is studio
13
13
  * state, not a data source.
14
+ *
15
+ * EC-479/EC-483 — **before a project's locale config is installed, this store
16
+ * holds `null`, not a locale.** It used to open on the engine's `en/de`
17
+ * `sampleLocaleConfig` fixture, which is the worst fallback in the family: the
18
+ * default locale is the literal STORAGE KEY for every localized field value
19
+ * (`documents.ts:88`), so a fabricated one writes a German project's content
20
+ * under `en` and correcting it later is a data migration, not a config edit. The
21
+ * locale is a deliberate choice at project creation (EC-483) and reaches the
22
+ * studio through {@link configureStudioLocales} from the project's own config —
23
+ * so "not loaded" is an honest state that surfaces, never an invented one that
24
+ * silently becomes a key.
25
+ */
26
+ /**
27
+ * `{ config, active }` once a project's locales are installed; `{ null, null }`
28
+ * before that. The two fields move together by construction — there is no state
29
+ * with a config but no active locale, or an active locale nobody declared.
14
30
  */
15
- interface StudioLocaleState {
31
+ export type StudioLocaleState = {
16
32
  readonly config: LocaleConfig;
17
33
  readonly active: Locale;
18
- }
34
+ } | {
35
+ readonly config: null;
36
+ readonly active: null;
37
+ };
19
38
  /** Snapshot of `{ config, active }` — stable identity until the next change. */
20
39
  export declare function getStudioLocaleState(): StudioLocaleState;
21
- /** The active studio locale every surface renders in. */
22
- export declare function getStudioLocale(): Locale;
23
- /** The project locale config driving the switcher options. */
24
- export declare function getStudioLocaleConfig(): LocaleConfig;
40
+ /**
41
+ * The active studio locale every surface renders in — `null` until a project's
42
+ * locale config is installed. A caller that genuinely needs a locale reads the
43
+ * PROJECT's own default (`CmsStore.localeConfig.default`, the content session's
44
+ * config); it must never substitute one of its own.
45
+ */
46
+ export declare function getStudioLocale(): Locale | null;
47
+ /**
48
+ * The project locale config driving the switcher options — `null` until one is
49
+ * installed (the studio has no locales of its own to offer).
50
+ */
51
+ export declare function getStudioLocaleConfig(): LocaleConfig | null;
25
52
  /**
26
53
  * Switch the ambient locale. Unknown locales are ignored (the switcher only
27
- * offers configured ones; programmatic callers get a no-op, never a crash).
54
+ * offers configured ones; programmatic callers get a no-op, never a crash), and
55
+ * so is any switch attempted before a config is installed — there is nothing to
56
+ * validate the locale against.
28
57
  */
29
58
  export declare function setStudioLocale(locale: Locale): void;
30
59
  /**
@@ -35,6 +64,9 @@ export declare function setStudioLocale(locale: Locale): void;
35
64
  export declare function configureStudioLocales(config: LocaleConfig): void;
36
65
  /** Subscribe to locale/config changes. Returns the unsubscribe function. */
37
66
  export declare function subscribeStudioLocale(listener: () => void): () => void;
38
- /** Reset to the sample defaults (tests only — module state is shared). */
67
+ /**
68
+ * Reset to the pre-config "not loaded" state (tests only — module state is
69
+ * shared). Never re-seeds a fixture config: a test that queries localized content
70
+ * must say which locale it means, exactly like a project does.
71
+ */
39
72
  export declare function resetStudioLocale(): void;
40
- export {};
@@ -1,8 +1,7 @@
1
- import { createLocaleConfig, isKnownLocale, sampleLocaleConfig } from '@elytracms/core/cms-core';
2
- let state = {
3
- config: sampleLocaleConfig,
4
- active: sampleLocaleConfig.default,
5
- };
1
+ import { createLocaleConfig, isKnownLocale } from '@elytracms/core/cms-core';
2
+ /** The pre-config state: no project locales have been installed yet. */
3
+ const NOT_LOADED = { config: null, active: null };
4
+ let state = NOT_LOADED;
6
5
  const listeners = new Set();
7
6
  function emit() {
8
7
  for (const listener of listeners)
@@ -12,22 +11,34 @@ function emit() {
12
11
  export function getStudioLocaleState() {
13
12
  return state;
14
13
  }
15
- /** The active studio locale every surface renders in. */
14
+ /**
15
+ * The active studio locale every surface renders in — `null` until a project's
16
+ * locale config is installed. A caller that genuinely needs a locale reads the
17
+ * PROJECT's own default (`CmsStore.localeConfig.default`, the content session's
18
+ * config); it must never substitute one of its own.
19
+ */
16
20
  export function getStudioLocale() {
17
21
  return state.active;
18
22
  }
19
- /** The project locale config driving the switcher options. */
23
+ /**
24
+ * The project locale config driving the switcher options — `null` until one is
25
+ * installed (the studio has no locales of its own to offer).
26
+ */
20
27
  export function getStudioLocaleConfig() {
21
28
  return state.config;
22
29
  }
23
30
  /**
24
31
  * Switch the ambient locale. Unknown locales are ignored (the switcher only
25
- * offers configured ones; programmatic callers get a no-op, never a crash).
32
+ * offers configured ones; programmatic callers get a no-op, never a crash), and
33
+ * so is any switch attempted before a config is installed — there is nothing to
34
+ * validate the locale against.
26
35
  */
27
36
  export function setStudioLocale(locale) {
37
+ if (state.config === null)
38
+ return;
28
39
  if (locale === state.active || !isKnownLocale(state.config, locale))
29
40
  return;
30
- state = { ...state, active: locale };
41
+ state = { config: state.config, active: locale };
31
42
  emit();
32
43
  }
33
44
  /**
@@ -37,10 +48,14 @@ export function setStudioLocale(locale) {
37
48
  */
38
49
  export function configureStudioLocales(config) {
39
50
  const normalized = createLocaleConfig({ default: config.default, locales: config.locales });
40
- const sameConfig = normalized.default === state.config.default &&
41
- normalized.locales.length === state.config.locales.length &&
42
- normalized.locales.every((locale, i) => locale === state.config.locales[i]);
43
- const active = isKnownLocale(normalized, state.active) ? state.active : normalized.default;
51
+ const current = state.config;
52
+ const sameConfig = current !== null &&
53
+ normalized.default === current.default &&
54
+ normalized.locales.length === current.locales.length &&
55
+ normalized.locales.every((locale, i) => locale === current.locales[i]);
56
+ const active = state.active !== null && isKnownLocale(normalized, state.active)
57
+ ? state.active
58
+ : normalized.default;
44
59
  if (sameConfig && active === state.active)
45
60
  return;
46
61
  state = { config: normalized, active };
@@ -53,9 +68,13 @@ export function subscribeStudioLocale(listener) {
53
68
  listeners.delete(listener);
54
69
  };
55
70
  }
56
- /** Reset to the sample defaults (tests only — module state is shared). */
71
+ /**
72
+ * Reset to the pre-config "not loaded" state (tests only — module state is
73
+ * shared). Never re-seeds a fixture config: a test that queries localized content
74
+ * must say which locale it means, exactly like a project does.
75
+ */
57
76
  export function resetStudioLocale() {
58
- state = { config: sampleLocaleConfig, active: sampleLocaleConfig.default };
77
+ state = NOT_LOADED;
59
78
  emit();
60
79
  }
61
80
  //# sourceMappingURL=locale-store.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"locale-store.js","sourceRoot":"","sources":["../../../src/lib/locale/locale-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AAsBhG,IAAI,KAAK,GAAsB;IAC7B,MAAM,EAAE,kBAAkB;IAC1B,MAAM,EAAE,kBAAkB,CAAC,OAAO;CACnC,CAAA;AAED,MAAM,SAAS,GAAG,IAAI,GAAG,EAAc,CAAA;AAEvC,SAAS,IAAI;IACX,KAAK,MAAM,QAAQ,IAAI,SAAS;QAAE,QAAQ,EAAE,CAAA;AAC9C,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,oBAAoB;IAClC,OAAO,KAAK,CAAA;AACd,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,eAAe;IAC7B,OAAO,KAAK,CAAC,MAAM,CAAA;AACrB,CAAC;AAED,8DAA8D;AAC9D,MAAM,UAAU,qBAAqB;IACnC,OAAO,KAAK,CAAC,MAAM,CAAA;AACrB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,MAAc;IAC5C,IAAI,MAAM,KAAK,KAAK,CAAC,MAAM,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC;QAAE,OAAM;IAC3E,KAAK,GAAG,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,CAAA;IACpC,IAAI,EAAE,CAAA;AACR,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAoB;IACzD,MAAM,UAAU,GAAG,kBAAkB,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAA;IAC3F,MAAM,UAAU,GACd,UAAU,CAAC,OAAO,KAAK,KAAK,CAAC,MAAM,CAAC,OAAO;QAC3C,UAAU,CAAC,OAAO,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM;QACzD,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAA;IAC7E,MAAM,MAAM,GAAG,aAAa,CAAC,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,OAAO,CAAA;IAC1F,IAAI,UAAU,IAAI,MAAM,KAAK,KAAK,CAAC,MAAM;QAAE,OAAM;IACjD,KAAK,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,CAAA;IACtC,IAAI,EAAE,CAAA;AACR,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,qBAAqB,CAAC,QAAoB;IACxD,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACvB,OAAO,GAAG,EAAE;QACV,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IAC5B,CAAC,CAAA;AACH,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,iBAAiB;IAC/B,KAAK,GAAG,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,EAAE,kBAAkB,CAAC,OAAO,EAAE,CAAA;IAC1E,IAAI,EAAE,CAAA;AACR,CAAC","sourcesContent":["import { createLocaleConfig, isKnownLocale, sampleLocaleConfig } from '@elytracms/core/cms-core'\nimport type { Locale, LocaleConfig } from '@elytracms/core/cms-core'\n\n/**\n * The studio's ambient locale (EC-176) — framework-free core.\n *\n * One studio-level active locale, set once via the locale switcher, that the\n * builder canvas preview, the CMS table, and the detail editor all follow.\n * Module-level (not React context) on purpose: the playground's content\n * client (`lib/data/collection-content`) reads it at query time without any\n * provider plumbing, and React surfaces subscribe through\n * `useStudioLocale` (`useSyncExternalStore`).\n *\n * Deliberately NOT part of `lib/data/data-store` — the locale is studio\n * state, not a data source.\n */\n\ninterface StudioLocaleState {\n readonly config: LocaleConfig\n readonly active: Locale\n}\n\nlet state: StudioLocaleState = {\n config: sampleLocaleConfig,\n active: sampleLocaleConfig.default,\n}\n\nconst listeners = new Set<() => void>()\n\nfunction emit(): void {\n for (const listener of listeners) listener()\n}\n\n/** Snapshot of `{ config, active }` — stable identity until the next change. */\nexport function getStudioLocaleState(): StudioLocaleState {\n return state\n}\n\n/** The active studio locale every surface renders in. */\nexport function getStudioLocale(): Locale {\n return state.active\n}\n\n/** The project locale config driving the switcher options. */\nexport function getStudioLocaleConfig(): LocaleConfig {\n return state.config\n}\n\n/**\n * Switch the ambient locale. Unknown locales are ignored (the switcher only\n * offers configured ones; programmatic callers get a no-op, never a crash).\n */\nexport function setStudioLocale(locale: Locale): void {\n if (locale === state.active || !isKnownLocale(state.config, locale)) return\n state = { ...state, active: locale }\n emit()\n}\n\n/**\n * Install the project's locale config (e.g. when a CMS session or builder\n * shell mounts with a starter kit's locales). The active locale is kept when\n * it is still configured, otherwise it snaps back to the new default.\n */\nexport function configureStudioLocales(config: LocaleConfig): void {\n const normalized = createLocaleConfig({ default: config.default, locales: config.locales })\n const sameConfig =\n normalized.default === state.config.default &&\n normalized.locales.length === state.config.locales.length &&\n normalized.locales.every((locale, i) => locale === state.config.locales[i])\n const active = isKnownLocale(normalized, state.active) ? state.active : normalized.default\n if (sameConfig && active === state.active) return\n state = { config: normalized, active }\n emit()\n}\n\n/** Subscribe to locale/config changes. Returns the unsubscribe function. */\nexport function subscribeStudioLocale(listener: () => void): () => void {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\n/** Reset to the sample defaults (tests only — module state is shared). */\nexport function resetStudioLocale(): void {\n state = { config: sampleLocaleConfig, active: sampleLocaleConfig.default }\n emit()\n}\n"]}
1
+ {"version":3,"file":"locale-store.js","sourceRoot":"","sources":["../../../src/lib/locale/locale-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAqC5E,wEAAwE;AACxE,MAAM,UAAU,GAAsB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAA;AAEpE,IAAI,KAAK,GAAsB,UAAU,CAAA;AAEzC,MAAM,SAAS,GAAG,IAAI,GAAG,EAAc,CAAA;AAEvC,SAAS,IAAI;IACX,KAAK,MAAM,QAAQ,IAAI,SAAS;QAAE,QAAQ,EAAE,CAAA;AAC9C,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,oBAAoB;IAClC,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe;IAC7B,OAAO,KAAK,CAAC,MAAM,CAAA;AACrB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB;IACnC,OAAO,KAAK,CAAC,MAAM,CAAA;AACrB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,MAAc;IAC5C,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI;QAAE,OAAM;IACjC,IAAI,MAAM,KAAK,KAAK,CAAC,MAAM,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC;QAAE,OAAM;IAC3E,KAAK,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAA;IAChD,IAAI,EAAE,CAAA;AACR,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAoB;IACzD,MAAM,UAAU,GAAG,kBAAkB,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAA;IAC3F,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAA;IAC5B,MAAM,UAAU,GACd,OAAO,KAAK,IAAI;QAChB,UAAU,CAAC,OAAO,KAAK,OAAO,CAAC,OAAO;QACtC,UAAU,CAAC,OAAO,CAAC,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,MAAM;QACpD,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAA;IACxE,MAAM,MAAM,GACV,KAAK,CAAC,MAAM,KAAK,IAAI,IAAI,aAAa,CAAC,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC;QAC9D,CAAC,CAAC,KAAK,CAAC,MAAM;QACd,CAAC,CAAC,UAAU,CAAC,OAAO,CAAA;IACxB,IAAI,UAAU,IAAI,MAAM,KAAK,KAAK,CAAC,MAAM;QAAE,OAAM;IACjD,KAAK,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,CAAA;IACtC,IAAI,EAAE,CAAA;AACR,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,qBAAqB,CAAC,QAAoB;IACxD,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACvB,OAAO,GAAG,EAAE;QACV,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IAC5B,CAAC,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB;IAC/B,KAAK,GAAG,UAAU,CAAA;IAClB,IAAI,EAAE,CAAA;AACR,CAAC","sourcesContent":["import { createLocaleConfig, isKnownLocale } from '@elytracms/core/cms-core'\nimport type { Locale, LocaleConfig } from '@elytracms/core/cms-core'\n\n/**\n * The studio's ambient locale (EC-176) — framework-free core.\n *\n * One studio-level active locale, set once via the locale switcher, that the\n * builder canvas preview, the CMS table, and the detail editor all follow.\n * Module-level (not React context) on purpose: the playground's content\n * client (`lib/data/collection-content`) reads it at query time without any\n * provider plumbing, and React surfaces subscribe through\n * `useStudioLocale` (`useSyncExternalStore`).\n *\n * Deliberately NOT part of `lib/data/data-store` — the locale is studio\n * state, not a data source.\n *\n * EC-479/EC-483 — **before a project's locale config is installed, this store\n * holds `null`, not a locale.** It used to open on the engine's `en/de`\n * `sampleLocaleConfig` fixture, which is the worst fallback in the family: the\n * default locale is the literal STORAGE KEY for every localized field value\n * (`documents.ts:88`), so a fabricated one writes a German project's content\n * under `en` and correcting it later is a data migration, not a config edit. The\n * locale is a deliberate choice at project creation (EC-483) and reaches the\n * studio through {@link configureStudioLocales} from the project's own config —\n * so \"not loaded\" is an honest state that surfaces, never an invented one that\n * silently becomes a key.\n */\n\n/**\n * `{ config, active }` once a project's locales are installed; `{ null, null }`\n * before that. The two fields move together by construction — there is no state\n * with a config but no active locale, or an active locale nobody declared.\n */\nexport type StudioLocaleState =\n | { readonly config: LocaleConfig; readonly active: Locale }\n | { readonly config: null; readonly active: null }\n\n/** The pre-config state: no project locales have been installed yet. */\nconst NOT_LOADED: StudioLocaleState = { config: null, active: null }\n\nlet state: StudioLocaleState = NOT_LOADED\n\nconst listeners = new Set<() => void>()\n\nfunction emit(): void {\n for (const listener of listeners) listener()\n}\n\n/** Snapshot of `{ config, active }` — stable identity until the next change. */\nexport function getStudioLocaleState(): StudioLocaleState {\n return state\n}\n\n/**\n * The active studio locale every surface renders in — `null` until a project's\n * locale config is installed. A caller that genuinely needs a locale reads the\n * PROJECT's own default (`CmsStore.localeConfig.default`, the content session's\n * config); it must never substitute one of its own.\n */\nexport function getStudioLocale(): Locale | null {\n return state.active\n}\n\n/**\n * The project locale config driving the switcher options — `null` until one is\n * installed (the studio has no locales of its own to offer).\n */\nexport function getStudioLocaleConfig(): LocaleConfig | null {\n return state.config\n}\n\n/**\n * Switch the ambient locale. Unknown locales are ignored (the switcher only\n * offers configured ones; programmatic callers get a no-op, never a crash), and\n * so is any switch attempted before a config is installed — there is nothing to\n * validate the locale against.\n */\nexport function setStudioLocale(locale: Locale): void {\n if (state.config === null) return\n if (locale === state.active || !isKnownLocale(state.config, locale)) return\n state = { config: state.config, active: locale }\n emit()\n}\n\n/**\n * Install the project's locale config (e.g. when a CMS session or builder\n * shell mounts with a starter kit's locales). The active locale is kept when\n * it is still configured, otherwise it snaps back to the new default.\n */\nexport function configureStudioLocales(config: LocaleConfig): void {\n const normalized = createLocaleConfig({ default: config.default, locales: config.locales })\n const current = state.config\n const sameConfig =\n current !== null &&\n normalized.default === current.default &&\n normalized.locales.length === current.locales.length &&\n normalized.locales.every((locale, i) => locale === current.locales[i])\n const active =\n state.active !== null && isKnownLocale(normalized, state.active)\n ? state.active\n : normalized.default\n if (sameConfig && active === state.active) return\n state = { config: normalized, active }\n emit()\n}\n\n/** Subscribe to locale/config changes. Returns the unsubscribe function. */\nexport function subscribeStudioLocale(listener: () => void): () => void {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\n/**\n * Reset to the pre-config \"not loaded\" state (tests only — module state is\n * shared). Never re-seeds a fixture config: a test that queries localized content\n * must say which locale it means, exactly like a project does.\n */\nexport function resetStudioLocale(): void {\n state = NOT_LOADED\n emit()\n}\n"]}
@@ -3,7 +3,9 @@
3
3
  * builder toolbar and the CMS surface header, both driving the same
4
4
  * studio-level active locale. Options come from the project locale config;
5
5
  * the default locale is marked. Renders nothing for single-locale projects —
6
- * no per-control locale ceremony anywhere else.
6
+ * no per-control locale ceremony anywhere else — nor before a project's locale
7
+ * config has been installed (EC-479: there is nothing to switch between, and the
8
+ * engine no longer supplies its own `en/de` fixture to pretend otherwise).
7
9
  */
8
10
  export declare function LocaleSwitcher({ className }: {
9
11
  className?: string;
@@ -8,11 +8,13 @@ import { useStudioLocale } from './use-studio-locale.js';
8
8
  * builder toolbar and the CMS surface header, both driving the same
9
9
  * studio-level active locale. Options come from the project locale config;
10
10
  * the default locale is marked. Renders nothing for single-locale projects —
11
- * no per-control locale ceremony anywhere else.
11
+ * no per-control locale ceremony anywhere else — nor before a project's locale
12
+ * config has been installed (EC-479: there is nothing to switch between, and the
13
+ * engine no longer supplies its own `en/de` fixture to pretend otherwise).
12
14
  */
13
15
  export function LocaleSwitcher({ className }) {
14
16
  const { locale, config, setLocale } = useStudioLocale();
15
- if (config.locales.length <= 1)
17
+ if (config === null || config.locales.length <= 1)
16
18
  return null;
17
19
  return (_jsxs("div", { role: "group", "aria-label": "Active locale", "data-testid": "locale-switcher", className: cn('flex items-center gap-1 rounded-md bg-muted/50 p-0.5', className), children: [_jsx(Globe, { "aria-hidden": true, className: "ml-1 size-3.5 self-center text-muted-foreground" }), config.locales.map((option) => {
18
20
  const isDefault = option === config.default;
@@ -1 +1 @@
1
- {"version":3,"file":"locale-switcher.js","sourceRoot":"","sources":["../../../src/lib/locale/locale-switcher.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAA;AACpC,OAAO,EAAE,EAAE,EAAE,MAAM,UAAU,CAAA;AAC7B,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAErD;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,EAAE,SAAS,EAA0B;IAClE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,eAAe,EAAE,CAAA;IACvD,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC;QAAE,OAAO,IAAI,CAAA;IAE3C,OAAO,CACL,eACE,IAAI,EAAC,OAAO,gBACD,eAAe,iBACd,iBAAiB,EAC7B,SAAS,EAAE,EAAE,CAAC,sDAAsD,EAAE,SAAS,CAAC,aAEhF,KAAC,KAAK,yBAAa,SAAS,EAAC,iDAAiD,GAAG,EAChF,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;gBAC7B,MAAM,SAAS,GAAG,MAAM,KAAK,MAAM,CAAC,OAAO,CAAA;gBAC3C,OAAO,CACL,kBAEE,IAAI,EAAC,QAAQ,kBACC,MAAM,KAAK,MAAM,EAC/B,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,eAAe,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC,CAAC,eAAe,CAAC,MAAM,CAAC,iBAC7E,iBAAiB,MAAM,EAAE,EACtC,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,EAChC,SAAS,EAAE,EAAE,CACX,2EAA2E,EAC3E,MAAM,KAAK,MAAM;wBACf,CAAC,CAAC,yCAAyC;wBAC3C,CAAC,CAAC,6CAA6C,CAClD,aAEA,MAAM,EACN,SAAS,CAAC,CAAC,CAAC,eAAM,SAAS,EAAC,iCAAiC,uBAAS,CAAC,CAAC,CAAC,IAAI,KAdzE,MAAM,CAeJ,CACV,CAAA;YACH,CAAC,CAAC,IACE,CACP,CAAA;AACH,CAAC","sourcesContent":["import { Globe } from 'lucide-react'\nimport { cn } from '../utils'\nimport { localeChipLabel } from './fallback'\nimport { useStudioLocale } from './use-studio-locale'\n\n/**\n * The ambient locale switcher (EC-176): one segmented control, mounted in the\n * builder toolbar and the CMS surface header, both driving the same\n * studio-level active locale. Options come from the project locale config;\n * the default locale is marked. Renders nothing for single-locale projects —\n * no per-control locale ceremony anywhere else.\n */\nexport function LocaleSwitcher({ className }: { className?: string }) {\n const { locale, config, setLocale } = useStudioLocale()\n if (config.locales.length <= 1) return null\n\n return (\n <div\n role=\"group\"\n aria-label=\"Active locale\"\n data-testid=\"locale-switcher\"\n className={cn('flex items-center gap-1 rounded-md bg-muted/50 p-0.5', className)}\n >\n <Globe aria-hidden className=\"ml-1 size-3.5 self-center text-muted-foreground\" />\n {config.locales.map((option) => {\n const isDefault = option === config.default\n return (\n <button\n key={option}\n type=\"button\"\n aria-pressed={option === locale}\n title={isDefault ? `${localeChipLabel(option)} (default locale)` : localeChipLabel(option)}\n data-testid={`locale-option-${option}`}\n onClick={() => setLocale(option)}\n className={cn(\n 'rounded-[5px] px-2 py-0.5 text-xs font-medium uppercase transition-colors',\n option === locale\n ? 'bg-background text-foreground shadow-sm'\n : 'text-muted-foreground hover:text-foreground',\n )}\n >\n {option}\n {isDefault ? <span className=\"ml-0.5 text-muted-foreground/70\">·</span> : null}\n </button>\n )\n })}\n </div>\n )\n}\n"]}
1
+ {"version":3,"file":"locale-switcher.js","sourceRoot":"","sources":["../../../src/lib/locale/locale-switcher.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAA;AACpC,OAAO,EAAE,EAAE,EAAE,MAAM,UAAU,CAAA;AAC7B,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAErD;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,EAAE,SAAS,EAA0B;IAClE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,eAAe,EAAE,CAAA;IACvD,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC;QAAE,OAAO,IAAI,CAAA;IAE9D,OAAO,CACL,eACE,IAAI,EAAC,OAAO,gBACD,eAAe,iBACd,iBAAiB,EAC7B,SAAS,EAAE,EAAE,CAAC,sDAAsD,EAAE,SAAS,CAAC,aAEhF,KAAC,KAAK,yBAAa,SAAS,EAAC,iDAAiD,GAAG,EAChF,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;gBAC7B,MAAM,SAAS,GAAG,MAAM,KAAK,MAAM,CAAC,OAAO,CAAA;gBAC3C,OAAO,CACL,kBAEE,IAAI,EAAC,QAAQ,kBACC,MAAM,KAAK,MAAM,EAC/B,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,eAAe,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC,CAAC,eAAe,CAAC,MAAM,CAAC,iBAC7E,iBAAiB,MAAM,EAAE,EACtC,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,EAChC,SAAS,EAAE,EAAE,CACX,2EAA2E,EAC3E,MAAM,KAAK,MAAM;wBACf,CAAC,CAAC,yCAAyC;wBAC3C,CAAC,CAAC,6CAA6C,CAClD,aAEA,MAAM,EACN,SAAS,CAAC,CAAC,CAAC,eAAM,SAAS,EAAC,iCAAiC,uBAAS,CAAC,CAAC,CAAC,IAAI,KAdzE,MAAM,CAeJ,CACV,CAAA;YACH,CAAC,CAAC,IACE,CACP,CAAA;AACH,CAAC","sourcesContent":["import { Globe } from 'lucide-react'\nimport { cn } from '../utils'\nimport { localeChipLabel } from './fallback'\nimport { useStudioLocale } from './use-studio-locale'\n\n/**\n * The ambient locale switcher (EC-176): one segmented control, mounted in the\n * builder toolbar and the CMS surface header, both driving the same\n * studio-level active locale. Options come from the project locale config;\n * the default locale is marked. Renders nothing for single-locale projects —\n * no per-control locale ceremony anywhere else — nor before a project's locale\n * config has been installed (EC-479: there is nothing to switch between, and the\n * engine no longer supplies its own `en/de` fixture to pretend otherwise).\n */\nexport function LocaleSwitcher({ className }: { className?: string }) {\n const { locale, config, setLocale } = useStudioLocale()\n if (config === null || config.locales.length <= 1) return null\n\n return (\n <div\n role=\"group\"\n aria-label=\"Active locale\"\n data-testid=\"locale-switcher\"\n className={cn('flex items-center gap-1 rounded-md bg-muted/50 p-0.5', className)}\n >\n <Globe aria-hidden className=\"ml-1 size-3.5 self-center text-muted-foreground\" />\n {config.locales.map((option) => {\n const isDefault = option === config.default\n return (\n <button\n key={option}\n type=\"button\"\n aria-pressed={option === locale}\n title={isDefault ? `${localeChipLabel(option)} (default locale)` : localeChipLabel(option)}\n data-testid={`locale-option-${option}`}\n onClick={() => setLocale(option)}\n className={cn(\n 'rounded-[5px] px-2 py-0.5 text-xs font-medium uppercase transition-colors',\n option === locale\n ? 'bg-background text-foreground shadow-sm'\n : 'text-muted-foreground hover:text-foreground',\n )}\n >\n {option}\n {isDefault ? <span className=\"ml-0.5 text-muted-foreground/70\">·</span> : null}\n </button>\n )\n })}\n </div>\n )\n}\n"]}
@@ -1,9 +1,14 @@
1
1
  import type { Locale, LocaleConfig } from '@elytracms/core/cms-core';
2
2
  export interface StudioLocale {
3
- /** The ambient active locale every studio surface renders in. */
4
- locale: Locale;
5
- /** The project locale config driving the switcher options. */
6
- config: LocaleConfig;
3
+ /**
4
+ * The ambient active locale every studio surface renders in — `null` before a
5
+ * project's locale config is installed (EC-479: the engine no longer opens on
6
+ * its own `en/de` fixture). A surface that must render in *some* locale reads
7
+ * the project's own default; it never invents one.
8
+ */
9
+ locale: Locale | null;
10
+ /** The project locale config driving the switcher options — `null` until installed. */
11
+ config: LocaleConfig | null;
7
12
  setLocale: (locale: Locale) => void;
8
13
  }
9
14
  /** Subscribe a React surface to the studio's ambient locale (EC-176). */
@@ -1 +1 @@
1
- {"version":3,"file":"use-studio-locale.js","sourceRoot":"","sources":["../../../src/lib/locale/use-studio-locale.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAA;AAE5C,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,qBAAqB,GACtB,MAAM,gBAAgB,CAAA;AAUvB,yEAAyE;AACzE,MAAM,UAAU,eAAe;IAC7B,MAAM,KAAK,GAAG,oBAAoB,CAChC,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,CACrB,CAAA;IACD,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,SAAS,EAAE,eAAe,EAAE,CAAA;AACnF,CAAC","sourcesContent":["import { useSyncExternalStore } from 'react'\nimport type { Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport {\n getStudioLocaleState,\n setStudioLocale,\n subscribeStudioLocale,\n} from './locale-store'\n\nexport interface StudioLocale {\n /** The ambient active locale every studio surface renders in. */\n locale: Locale\n /** The project locale config driving the switcher options. */\n config: LocaleConfig\n setLocale: (locale: Locale) => void\n}\n\n/** Subscribe a React surface to the studio's ambient locale (EC-176). */\nexport function useStudioLocale(): StudioLocale {\n const state = useSyncExternalStore(\n subscribeStudioLocale,\n getStudioLocaleState,\n getStudioLocaleState,\n )\n return { locale: state.active, config: state.config, setLocale: setStudioLocale }\n}\n"]}
1
+ {"version":3,"file":"use-studio-locale.js","sourceRoot":"","sources":["../../../src/lib/locale/use-studio-locale.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAA;AAE5C,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,qBAAqB,GACtB,MAAM,gBAAgB,CAAA;AAevB,yEAAyE;AACzE,MAAM,UAAU,eAAe;IAC7B,MAAM,KAAK,GAAG,oBAAoB,CAChC,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,CACrB,CAAA;IACD,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,SAAS,EAAE,eAAe,EAAE,CAAA;AACnF,CAAC","sourcesContent":["import { useSyncExternalStore } from 'react'\nimport type { Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport {\n getStudioLocaleState,\n setStudioLocale,\n subscribeStudioLocale,\n} from './locale-store'\n\nexport interface StudioLocale {\n /**\n * The ambient active locale every studio surface renders in — `null` before a\n * project's locale config is installed (EC-479: the engine no longer opens on\n * its own `en/de` fixture). A surface that must render in *some* locale reads\n * the project's own default; it never invents one.\n */\n locale: Locale | null\n /** The project locale config driving the switcher options — `null` until installed. */\n config: LocaleConfig | null\n setLocale: (locale: Locale) => void\n}\n\n/** Subscribe a React surface to the studio's ambient locale (EC-176). */\nexport function useStudioLocale(): StudioLocale {\n const state = useSyncExternalStore(\n subscribeStudioLocale,\n getStudioLocaleState,\n getStudioLocaleState,\n )\n return { locale: state.active, config: state.config, setLocale: setStudioLocale }\n}\n"]}
@@ -1,6 +1,7 @@
1
1
  import { activeDeploymentKey, selectPersistenceAdapter } from '../persistence/index.js';
2
2
  import { attachSessionExpiryReporting } from '../session-expiry.js';
3
3
  import { attachConfiguredRevalidationWebhooks } from '../webhooks/index.js';
4
+ import { sessionValidateDocument } from '../cms/document-gate.js';
4
5
  import { attachReferenceIndex, createChangesets, createOperations, createRoleAuthorization, } from './operations-package.js';
5
6
  /**
6
7
  * A no-op {@link ReferenceIndexAttachment} for self-maintaining adapters (EC-277):
@@ -15,7 +16,17 @@ export function createStudioOperations(adapter, options = {}) {
15
16
  const authorized = options.actor
16
17
  ? { actor: options.actor, authorize: createRoleAuthorization(adapter.members) }
17
18
  : {};
18
- const operations = createOperations({ adapter, ...authorized });
19
+ // EC-497: the publish gate validates with the CMS session's canonical option
20
+ // bag — the same one the detail editor, bulk publish, cell edit, import and
21
+ // every AI path use — instead of deciding its own (weaker, mode-dependent)
22
+ // strength off the adapter-served schema record. The seam reads a validator the
23
+ // mounted session published; with no session it returns `undefined` and the gate
24
+ // takes its documented fallback.
25
+ const operations = createOperations({
26
+ adapter,
27
+ validateDocument: sessionValidateDocument,
28
+ ...authorized,
29
+ });
19
30
  // EC-277: when the adapter maintains the index in its own write path (AD-6 "at the
20
31
  // source" — the Convex provider does this server-side, so a raw `convex run` is
21
32
  // caught too), the client-side subscriber would only double the work. Attach it
@@ -25,7 +36,13 @@ export function createStudioOperations(adapter, options = {}) {
25
36
  const references = adapter.maintainsReferenceIndex
26
37
  ? noopReferenceIndexAttachment()
27
38
  : attachReferenceIndex(operations.events, adapter);
28
- const changesets = createChangesets({ adapter, events: operations.events, ...authorized });
39
+ const changesets = createChangesets({
40
+ adapter,
41
+ events: operations.events,
42
+ // EC-497: a changeset-driven publish is gated identically — same seam, same bag.
43
+ validateDocument: sessionValidateDocument,
44
+ ...authorized,
45
+ });
29
46
  // EC-146 instant publish: when a host revalidate endpoint is configured via
30
47
  // env, every successful publish-shaped command (document upsert/delete/
31
48
  // setState, graph publish/unpublish) fires a signed revalidation webhook.
@@ -1 +1 @@
1
- {"version":3,"file":"studio-operations.js","sourceRoot":"","sources":["../../../src/lib/operations/studio-operations.ts"],"names":[],"mappings":"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","sourcesContent":["import type { PersistenceAdapter } from '@elytracms/core/persistence'\nimport { activeDeploymentKey, selectPersistenceAdapter } from '../persistence'\nimport { attachSessionExpiryReporting } from '../session-expiry'\nimport { attachConfiguredRevalidationWebhooks } from '../webhooks'\nimport {\n attachReferenceIndex,\n createChangesets,\n createOperations,\n createRoleAuthorization,\n} from './operations-package'\nimport type {\n ChangesetOperations,\n OperationsClient,\n Principal,\n ReferenceIndexAttachment,\n} from './operations-package'\n\n/**\n * The studio's single operations bootstrap (EC-141, vision AD-7).\n *\n * One `OperationsClient` + one `ChangesetOperations` instance, built over the\n * selected persistence adapter, shared by every store and surface in the app.\n * The UI never talks to a repository for writes — every mutation is a typed\n * operation command (validate → authorize → mutate → event), so the studio\n * dogfoods the same seam plugins, AI changesets, and the CLI use.\n *\n * The EC-155 reference index is attached to the shared event bus here, so any\n * document/graph/publishing command issued anywhere in the studio keeps the\n * index maintained.\n */\nexport interface StudioOperations {\n readonly operations: OperationsClient\n readonly changesets: ChangesetOperations\n /**\n * The raw adapter, exposed for *transport and read paths only*: blob byte\n * upload/download (`adapter.blobs`) and existing read stores. Studio code\n * must not call repository write methods on it — writes go through\n * `operations`.\n */\n readonly adapter: PersistenceAdapter\n /** EC-155 maintained-on-write index attachment (flushable in tests). */\n readonly references: ReferenceIndexAttachment\n}\n\nexport interface CreateStudioOperationsOptions {\n /**\n * The signed-in principal (EC-150). When present, the EC-150 role hook\n * (`createRoleAuthorization` over `adapter.members`) becomes the\n * authorization chokepoint for every command and query — viewers get\n * structured denials on mutations, editors on administration. Without an\n * actor the client runs pass-through (seeds, fixtures, tests that exercise\n * domain logic rather than permissions); the route guard keeps\n * unauthenticated users away from every operation surface.\n */\n actor?: Principal\n}\n\n/**\n * A no-op {@link ReferenceIndexAttachment} for self-maintaining adapters (EC-277):\n * nothing is subscribed, so `detach` and `flush` are trivially satisfied (the index\n * is kept current in the adapter's write path, not by a queued subscriber).\n */\nfunction noopReferenceIndexAttachment(): ReferenceIndexAttachment {\n return { detach: () => {}, flush: async () => {} }\n}\n\n/** Build a studio operations bundle over an adapter (exported for tests). */\nexport function createStudioOperations(\n adapter: PersistenceAdapter,\n options: CreateStudioOperationsOptions = {},\n): StudioOperations {\n const authorized = options.actor\n ? { actor: options.actor, authorize: createRoleAuthorization(adapter.members) }\n : {}\n const operations = createOperations({ adapter, ...authorized })\n // EC-277: when the adapter maintains the index in its own write path (AD-6 \"at the\n // source\" — the Convex provider does this server-side, so a raw `convex run` is\n // caught too), the client-side subscriber would only double the work. Attach it\n // ONLY for providers that don't self-maintain. Where it does run, extraction is a\n // schema-free, manifest-free walk over self-describing content, so it needs no\n // registry (compositions self-typed their relation/asset props on write).\n const references = adapter.maintainsReferenceIndex\n ? noopReferenceIndexAttachment()\n : attachReferenceIndex(operations.events, adapter)\n const changesets = createChangesets({ adapter, events: operations.events, ...authorized })\n // EC-146 instant publish: when a host revalidate endpoint is configured via\n // env, every successful publish-shaped command (document upsert/delete/\n // setState, graph publish/unpublish) fires a signed revalidation webhook.\n // No env (dev/tests) → no-op.\n attachConfiguredRevalidationWebhooks(operations.events)\n // EC-403: session-expiry detection rides the SAME lifecycle bus — every failed\n // command/query/changeset operation is checked against the session-expired\n // predicate here, so expiry raises the ONE app-level blocking re-auth state no\n // matter which surface issued the operation (per-surface banners can't cover\n // surfaces that render no banner).\n attachSessionExpiryReporting(operations.events)\n return { operations, changesets, adapter, references }\n}\n\n// One adapter + one operations bundle per backend (EC-179): keyed by the active\n// deployment so switching to a project on another Convex deployment selects (or\n// lazily builds) that backend's client, rather than reusing the first one.\nconst adaptersByKey = new Map<string, Promise<PersistenceAdapter>>()\nconst instancesByKey = new Map<string, Promise<StudioOperations>>()\nlet currentActor: Principal | null = null\n\n/**\n * The active backend key, tolerant of an unconfigured env: the real app never\n * reaches `getStudioOperations` while misconfigured (the `DeploymentSetupGate` renders a\n * boot error first), but tests inject a bundle directly via the seam below — so\n * a missing backend resolves to one stable key rather than throwing.\n */\nfunction deploymentKey(): string {\n try {\n return activeDeploymentKey()\n } catch {\n return '__unconfigured__'\n }\n}\n\nfunction getAdapter(key: string): Promise<PersistenceAdapter> {\n let adapter = adaptersByKey.get(key)\n if (!adapter) {\n adapter = selectPersistenceAdapter()\n adaptersByKey.set(key, adapter)\n }\n return adapter\n}\n\n/**\n * The shared studio operations client for the active backend (lazily\n * bootstrapped, cached per deployment). In-memory mode this owns the one seeded\n * adapter the whole session mutates; against Convex it owns one client per\n * deployment — the active project (EC-179) selects which.\n */\nexport function getStudioOperations(): Promise<StudioOperations> {\n const key = deploymentKey()\n let instance = instancesByKey.get(key)\n if (!instance) {\n const actor = currentActor\n instance = getAdapter(key).then((adapter) =>\n createStudioOperations(adapter, actor ? { actor } : {}),\n )\n instancesByKey.set(key, instance)\n }\n return instance\n}\n\n/**\n * Bind (or clear) the session principal (EC-150). Called by the auth context\n * on sign-in/sign-out. Rebuilds the operations clients over the *same* adapters\n * — the persisted state (and, in-memory, the whole session's edits) survives;\n * only the authorization identity changes. All cached backends are rebuilt so a\n * later switch picks up the new actor too.\n */\nexport function setStudioSession(actor: Principal | null): void {\n if (currentActor?.id === actor?.id) return // same user (or both null) — keep the clients\n currentActor = actor\n instancesByKey.clear()\n}\n\n/** Test seam: replace (or clear) the singleton for the active backend. */\nexport function setStudioOperationsForTests(next: StudioOperations | null): void {\n if (next) {\n instancesByKey.set(deploymentKey(), Promise.resolve(next))\n return\n }\n instancesByKey.clear()\n adaptersByKey.clear()\n currentActor = null\n}\n"]}
1
+ {"version":3,"file":"studio-operations.js","sourceRoot":"","sources":["../../../src/lib/operations/studio-operations.ts"],"names":[],"mappings":"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","sourcesContent":["import type { PersistenceAdapter } from '@elytracms/core/persistence'\nimport { activeDeploymentKey, selectPersistenceAdapter } from '../persistence'\nimport { attachSessionExpiryReporting } from '../session-expiry'\nimport { attachConfiguredRevalidationWebhooks } from '../webhooks'\nimport { sessionValidateDocument } from '../cms/document-gate'\nimport {\n attachReferenceIndex,\n createChangesets,\n createOperations,\n createRoleAuthorization,\n} from './operations-package'\nimport type {\n ChangesetOperations,\n OperationsClient,\n Principal,\n ReferenceIndexAttachment,\n} from './operations-package'\n\n/**\n * The studio's single operations bootstrap (EC-141, vision AD-7).\n *\n * One `OperationsClient` + one `ChangesetOperations` instance, built over the\n * selected persistence adapter, shared by every store and surface in the app.\n * The UI never talks to a repository for writes — every mutation is a typed\n * operation command (validate → authorize → mutate → event), so the studio\n * dogfoods the same seam plugins, AI changesets, and the CLI use.\n *\n * The EC-155 reference index is attached to the shared event bus here, so any\n * document/graph/publishing command issued anywhere in the studio keeps the\n * index maintained.\n */\nexport interface StudioOperations {\n readonly operations: OperationsClient\n readonly changesets: ChangesetOperations\n /**\n * The raw adapter, exposed for *transport and read paths only*: blob byte\n * upload/download (`adapter.blobs`) and existing read stores. Studio code\n * must not call repository write methods on it — writes go through\n * `operations`.\n */\n readonly adapter: PersistenceAdapter\n /** EC-155 maintained-on-write index attachment (flushable in tests). */\n readonly references: ReferenceIndexAttachment\n}\n\nexport interface CreateStudioOperationsOptions {\n /**\n * The signed-in principal (EC-150). When present, the EC-150 role hook\n * (`createRoleAuthorization` over `adapter.members`) becomes the\n * authorization chokepoint for every command and query — viewers get\n * structured denials on mutations, editors on administration. Without an\n * actor the client runs pass-through (seeds, fixtures, tests that exercise\n * domain logic rather than permissions); the route guard keeps\n * unauthenticated users away from every operation surface.\n */\n actor?: Principal\n}\n\n/**\n * A no-op {@link ReferenceIndexAttachment} for self-maintaining adapters (EC-277):\n * nothing is subscribed, so `detach` and `flush` are trivially satisfied (the index\n * is kept current in the adapter's write path, not by a queued subscriber).\n */\nfunction noopReferenceIndexAttachment(): ReferenceIndexAttachment {\n return { detach: () => {}, flush: async () => {} }\n}\n\n/** Build a studio operations bundle over an adapter (exported for tests). */\nexport function createStudioOperations(\n adapter: PersistenceAdapter,\n options: CreateStudioOperationsOptions = {},\n): StudioOperations {\n const authorized = options.actor\n ? { actor: options.actor, authorize: createRoleAuthorization(adapter.members) }\n : {}\n // EC-497: the publish gate validates with the CMS session's canonical option\n // bag — the same one the detail editor, bulk publish, cell edit, import and\n // every AI path use — instead of deciding its own (weaker, mode-dependent)\n // strength off the adapter-served schema record. The seam reads a validator the\n // mounted session published; with no session it returns `undefined` and the gate\n // takes its documented fallback.\n const operations = createOperations({\n adapter,\n validateDocument: sessionValidateDocument,\n ...authorized,\n })\n // EC-277: when the adapter maintains the index in its own write path (AD-6 \"at the\n // source\" — the Convex provider does this server-side, so a raw `convex run` is\n // caught too), the client-side subscriber would only double the work. Attach it\n // ONLY for providers that don't self-maintain. Where it does run, extraction is a\n // schema-free, manifest-free walk over self-describing content, so it needs no\n // registry (compositions self-typed their relation/asset props on write).\n const references = adapter.maintainsReferenceIndex\n ? noopReferenceIndexAttachment()\n : attachReferenceIndex(operations.events, adapter)\n const changesets = createChangesets({\n adapter,\n events: operations.events,\n // EC-497: a changeset-driven publish is gated identically — same seam, same bag.\n validateDocument: sessionValidateDocument,\n ...authorized,\n })\n // EC-146 instant publish: when a host revalidate endpoint is configured via\n // env, every successful publish-shaped command (document upsert/delete/\n // setState, graph publish/unpublish) fires a signed revalidation webhook.\n // No env (dev/tests) → no-op.\n attachConfiguredRevalidationWebhooks(operations.events)\n // EC-403: session-expiry detection rides the SAME lifecycle bus — every failed\n // command/query/changeset operation is checked against the session-expired\n // predicate here, so expiry raises the ONE app-level blocking re-auth state no\n // matter which surface issued the operation (per-surface banners can't cover\n // surfaces that render no banner).\n attachSessionExpiryReporting(operations.events)\n return { operations, changesets, adapter, references }\n}\n\n// One adapter + one operations bundle per backend (EC-179): keyed by the active\n// deployment so switching to a project on another Convex deployment selects (or\n// lazily builds) that backend's client, rather than reusing the first one.\nconst adaptersByKey = new Map<string, Promise<PersistenceAdapter>>()\nconst instancesByKey = new Map<string, Promise<StudioOperations>>()\nlet currentActor: Principal | null = null\n\n/**\n * The active backend key, tolerant of an unconfigured env: the real app never\n * reaches `getStudioOperations` while misconfigured (the `DeploymentSetupGate` renders a\n * boot error first), but tests inject a bundle directly via the seam below — so\n * a missing backend resolves to one stable key rather than throwing.\n */\nfunction deploymentKey(): string {\n try {\n return activeDeploymentKey()\n } catch {\n return '__unconfigured__'\n }\n}\n\nfunction getAdapter(key: string): Promise<PersistenceAdapter> {\n let adapter = adaptersByKey.get(key)\n if (!adapter) {\n adapter = selectPersistenceAdapter()\n adaptersByKey.set(key, adapter)\n }\n return adapter\n}\n\n/**\n * The shared studio operations client for the active backend (lazily\n * bootstrapped, cached per deployment). In-memory mode this owns the one seeded\n * adapter the whole session mutates; against Convex it owns one client per\n * deployment — the active project (EC-179) selects which.\n */\nexport function getStudioOperations(): Promise<StudioOperations> {\n const key = deploymentKey()\n let instance = instancesByKey.get(key)\n if (!instance) {\n const actor = currentActor\n instance = getAdapter(key).then((adapter) =>\n createStudioOperations(adapter, actor ? { actor } : {}),\n )\n instancesByKey.set(key, instance)\n }\n return instance\n}\n\n/**\n * Bind (or clear) the session principal (EC-150). Called by the auth context\n * on sign-in/sign-out. Rebuilds the operations clients over the *same* adapters\n * — the persisted state (and, in-memory, the whole session's edits) survives;\n * only the authorization identity changes. All cached backends are rebuilt so a\n * later switch picks up the new actor too.\n */\nexport function setStudioSession(actor: Principal | null): void {\n if (currentActor?.id === actor?.id) return // same user (or both null) — keep the clients\n currentActor = actor\n instancesByKey.clear()\n}\n\n/** Test seam: replace (or clear) the singleton for the active backend. */\nexport function setStudioOperationsForTests(next: StudioOperations | null): void {\n if (next) {\n instancesByKey.set(deploymentKey(), Promise.resolve(next))\n return\n }\n instancesByKey.clear()\n adaptersByKey.clear()\n currentActor = null\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  import type { ConvexHttpClient } from 'convex/browser';
2
2
  import type { CollectionDef } from '@elytracms/core/cms-core';
3
- import type { AssetRepository, BackendStatus, BackendValidationInput, BackendValidationResult, BlobStorageAdapter, CliTokenRepository, CmsRepository, DeliveryRepository, MembersRepository, PersistenceAdapter, ReferenceIndexRepository, RedirectRepository, UserRepository, InviteRepository } from '@elytracms/core/persistence';
3
+ import type { AssetRepository, BackendStatus, BackendValidationInput, BackendValidationResult, BlobStorageAdapter, CmsRepository, DeliveryRepository, MembersRepository, PersistenceAdapter, ReferenceIndexRepository, RedirectRepository, UserRepository, InviteRepository } from '@elytracms/core/persistence';
4
4
  /**
5
5
  * Convex-backed {@link PersistenceAdapter} (brief §10, EC-039). Every repository
6
6
  * is implemented against the Convex functions in `apps/builder/convex/*`, mapping
@@ -19,8 +19,6 @@ export declare class ConvexPersistenceAdapter implements PersistenceAdapter {
19
19
  readonly users: UserRepository;
20
20
  /** Editorial redirects rail — data-backed 301/302 forwards (EC-303). */
21
21
  readonly redirects: RedirectRepository;
22
- /** Revocable CLI sync tokens, stored hashed (EC-150). */
23
- readonly cliTokens: CliTokenRepository;
24
22
  /** Convex file storage behind the EC-133 blob boundary (EC-151). */
25
23
  readonly blobs: BlobStorageAdapter;
26
24
  /** Delivery-pipeline (instant-publish) health, read-only (EC-268/EC-329). */
@@ -612,58 +612,6 @@ class ConvexInviteRepository {
612
612
  }
613
613
  }
614
614
  }
615
- function cliTokenRecordFromDoc(doc) {
616
- return {
617
- id: doc.tokenId,
618
- label: doc.label,
619
- tokenHash: doc.tokenHash,
620
- createdAt: doc.createdAt,
621
- ...(doc.revokedAt ? { revokedAt: doc.revokedAt } : {}),
622
- };
623
- }
624
- class ConvexCliTokenRepository {
625
- client;
626
- clock;
627
- constructor(client, clock = realClock) {
628
- this.client = client;
629
- this.clock = clock;
630
- }
631
- async createToken(input) {
632
- const doc = (await this.client.mutation(mutationRef('cliTokens:create'), {
633
- tokenId: newId('tok'),
634
- label: input.label,
635
- tokenHash: input.tokenHash,
636
- createdAt: this.clock(),
637
- }));
638
- return cliTokenRecordFromDoc(doc);
639
- }
640
- async listTokens() {
641
- const docs = (await this.client.query(queryRef('cliTokens:list'), {}));
642
- return docs.map(cliTokenRecordFromDoc);
643
- }
644
- async revokeToken(tokenId) {
645
- try {
646
- const doc = (await this.client.mutation(mutationRef('cliTokens:revoke'), {
647
- tokenId,
648
- revokedAt: this.clock(),
649
- }));
650
- return cliTokenRecordFromDoc(doc);
651
- }
652
- catch (error) {
653
- const message = error instanceof Error ? error.message : String(error);
654
- if (/not-found/i.test(message)) {
655
- throw new RecordNotFoundError('cli-token', tokenId);
656
- }
657
- throw error;
658
- }
659
- }
660
- async findByHash(tokenHash) {
661
- const doc = (await this.client.query(queryRef('cliTokens:byHash'), {
662
- tokenHash,
663
- }));
664
- return doc ? cliTokenRecordFromDoc(doc) : undefined;
665
- }
666
- }
667
615
  function redirectEntryFromDoc(doc, projectId) {
668
616
  return {
669
617
  id: doc.redirectId,
@@ -764,8 +712,6 @@ export class ConvexPersistenceAdapter {
764
712
  users;
765
713
  /** Editorial redirects rail — data-backed 301/302 forwards (EC-303). */
766
714
  redirects;
767
- /** Revocable CLI sync tokens, stored hashed (EC-150). */
768
- cliTokens;
769
715
  /** Convex file storage behind the EC-133 blob boundary (EC-151). */
770
716
  blobs;
771
717
  /** Delivery-pipeline (instant-publish) health, read-only (EC-268/EC-329). */
@@ -794,7 +740,6 @@ export class ConvexPersistenceAdapter {
794
740
  this.users = new ConvexUserRepository(client);
795
741
  this.invites = new ConvexInviteRepository(client);
796
742
  this.redirects = new ConvexRedirectRepository(client);
797
- this.cliTokens = new ConvexCliTokenRepository(client);
798
743
  this.blobs = new ConvexBlobStorage(client);
799
744
  this.delivery = new ConvexDeliveryRepository(client);
800
745
  }