@elytracms/studio 0.0.79 → 0.0.81

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 (107) 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/schema.ts +0 -19
  6. package/convex/users.ts +1 -1
  7. package/dist/components/app-views/default-modules.d.ts +141 -44
  8. package/dist/components/app-views/default-modules.js +229 -65
  9. package/dist/components/app-views/default-modules.js.map +1 -1
  10. package/dist/components/app-views/module.d.ts +38 -0
  11. package/dist/components/app-views/module.js.map +1 -1
  12. package/dist/components/app-views/sidebar.d.ts +5 -2
  13. package/dist/components/app-views/sidebar.js +16 -5
  14. package/dist/components/app-views/sidebar.js.map +1 -1
  15. package/dist/components/app-views/studio-config.d.ts +27 -0
  16. package/dist/components/app-views/studio-config.js.map +1 -1
  17. package/dist/components/builder/canvas-render-context.js +7 -1
  18. package/dist/components/builder/canvas-render-context.js.map +1 -1
  19. package/dist/components/builder/reuse-context.js +7 -1
  20. package/dist/components/builder/reuse-context.js.map +1 -1
  21. package/dist/components/builder/reuse.d.ts +94 -16
  22. package/dist/components/builder/reuse.js +201 -19
  23. package/dist/components/builder/reuse.js.map +1 -1
  24. package/dist/components/cms/bulk-actions.js +3 -1
  25. package/dist/components/cms/bulk-actions.js.map +1 -1
  26. package/dist/components/cms/cell-edit.js +6 -2
  27. package/dist/components/cms/cell-edit.js.map +1 -1
  28. package/dist/components/cms/cms-store.d.ts +40 -13
  29. package/dist/components/cms/cms-store.js +50 -22
  30. package/dist/components/cms/cms-store.js.map +1 -1
  31. package/dist/components/cms/collection-table.js +2 -2
  32. package/dist/components/cms/collection-table.js.map +1 -1
  33. package/dist/components/cms/date-input-core.d.ts +21 -0
  34. package/dist/components/cms/date-input-core.js +20 -0
  35. package/dist/components/cms/date-input-core.js.map +1 -1
  36. package/dist/components/cms/date-input.d.ts +9 -1
  37. package/dist/components/cms/date-input.js +2 -2
  38. package/dist/components/cms/date-input.js.map +1 -1
  39. package/dist/components/cms/detail-editor.js +11 -5
  40. package/dist/components/cms/detail-editor.js.map +1 -1
  41. package/dist/components/cms/editable-cell.js +2 -1
  42. package/dist/components/cms/editable-cell.js.map +1 -1
  43. package/dist/components/cms/field-control.js +4 -2
  44. package/dist/components/cms/field-control.js.map +1 -1
  45. package/dist/components/cms/field-renderer.js +8 -1
  46. package/dist/components/cms/field-renderer.js.map +1 -1
  47. package/dist/components/cms/import-export.js +2 -1
  48. package/dist/components/cms/import-export.js.map +1 -1
  49. package/dist/components/cms/issues-pane.d.ts +4 -2
  50. package/dist/components/cms/issues-pane.js +6 -4
  51. package/dist/components/cms/issues-pane.js.map +1 -1
  52. package/dist/components/cms/scalar-value-editor.js +2 -1
  53. package/dist/components/cms/scalar-value-editor.js.map +1 -1
  54. package/dist/components/components/transfer-dialog.js +10 -12
  55. package/dist/components/components/transfer-dialog.js.map +1 -1
  56. package/dist/components/project-list.d.ts +9 -3
  57. package/dist/components/project-list.js +11 -5
  58. package/dist/components/project-list.js.map +1 -1
  59. package/dist/components/project-switcher.d.ts +3 -1
  60. package/dist/components/project-switcher.js +4 -2
  61. package/dist/components/project-switcher.js.map +1 -1
  62. package/dist/components/redirects/redirects-store.d.ts +21 -0
  63. package/dist/components/redirects/redirects-store.js +17 -4
  64. package/dist/components/redirects/redirects-store.js.map +1 -1
  65. package/dist/components/section-nav.js +102 -4
  66. package/dist/components/section-nav.js.map +1 -1
  67. package/dist/customization.d.ts +7 -0
  68. package/dist/customization.js.map +1 -1
  69. package/dist/lib/data/collection-content.d.ts +6 -1
  70. package/dist/lib/data/collection-content.js +50 -12
  71. package/dist/lib/data/collection-content.js.map +1 -1
  72. package/dist/lib/i18n/messages.de.js +132 -0
  73. package/dist/lib/i18n/messages.de.js.map +1 -1
  74. package/dist/lib/i18n/messages.en.d.ts +103 -0
  75. package/dist/lib/i18n/messages.en.js +146 -0
  76. package/dist/lib/i18n/messages.en.js.map +1 -1
  77. package/dist/lib/locale/binding-fallback.d.ts +8 -1
  78. package/dist/lib/locale/binding-fallback.js +8 -1
  79. package/dist/lib/locale/binding-fallback.js.map +1 -1
  80. package/dist/lib/locale/locale-store.d.ts +41 -9
  81. package/dist/lib/locale/locale-store.js +34 -15
  82. package/dist/lib/locale/locale-store.js.map +1 -1
  83. package/dist/lib/locale/locale-switcher.d.ts +3 -1
  84. package/dist/lib/locale/locale-switcher.js +4 -2
  85. package/dist/lib/locale/locale-switcher.js.map +1 -1
  86. package/dist/lib/locale/use-studio-locale.d.ts +9 -4
  87. package/dist/lib/locale/use-studio-locale.js.map +1 -1
  88. package/dist/lib/persistence/convex-adapter.d.ts +1 -3
  89. package/dist/lib/persistence/convex-adapter.js +0 -55
  90. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  91. package/dist/lib/persona/labels.d.ts +42 -1
  92. package/dist/lib/persona/labels.js +57 -0
  93. package/dist/lib/persona/labels.js.map +1 -1
  94. package/dist/lib/routes/route-issues.d.ts +9 -0
  95. package/dist/lib/routes/route-issues.js +117 -0
  96. package/dist/lib/routes/route-issues.js.map +1 -0
  97. package/dist/lib/workspace/config-issues.d.ts +54 -0
  98. package/dist/lib/workspace/config-issues.js +70 -0
  99. package/dist/lib/workspace/config-issues.js.map +1 -0
  100. package/dist/lib/workspace/index.d.ts +9 -0
  101. package/dist/lib/workspace/index.js +11 -0
  102. package/dist/lib/workspace/index.js.map +1 -1
  103. package/dist/lib/workspace/switcher-source.d.ts +20 -3
  104. package/dist/lib/workspace/switcher-source.js.map +1 -1
  105. package/package.json +2 -2
  106. package/convex/cliTokens.ts +0 -63
  107. package/convex/sync.ts +0 -297
@@ -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":"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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 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,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
  }