@elytracms/studio 0.0.86 → 0.0.87

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 (203) hide show
  1. package/convex/asset-revalidate.test.ts +186 -0
  2. package/convex/assets.ts +53 -19
  3. package/convex/cms.ts +22 -0
  4. package/convex/delivery.ts +33 -116
  5. package/convex/functions.ts +3 -3
  6. package/convex/rail-write-parity.test.ts +180 -0
  7. package/convex/redirects.ts +33 -11
  8. package/convex/revalidate.ts +7 -5
  9. package/convex/triggers.ts +53 -3
  10. package/dist/components/app-views/default-modules.d.ts +20 -20
  11. package/dist/components/app-views/default-modules.js +27 -48
  12. package/dist/components/app-views/default-modules.js.map +1 -1
  13. package/dist/components/app-views/studio-config.d.ts +5 -6
  14. package/dist/components/app-views/studio-config.js.map +1 -1
  15. package/dist/components/assets/asset-metadata.d.ts +28 -19
  16. package/dist/components/assets/asset-metadata.js +24 -31
  17. package/dist/components/assets/asset-metadata.js.map +1 -1
  18. package/dist/components/assets/asset-selection.d.ts +24 -5
  19. package/dist/components/assets/asset-selection.js +39 -9
  20. package/dist/components/assets/asset-selection.js.map +1 -1
  21. package/dist/components/assets/assets-collection-view-source.d.ts +17 -14
  22. package/dist/components/assets/assets-collection-view-source.js +25 -31
  23. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  24. package/dist/components/builder/blocks-panel.js +3 -3
  25. package/dist/components/builder/blocks-panel.js.map +1 -1
  26. package/dist/components/builder/builder-store.d.ts +25 -1
  27. package/dist/components/builder/builder-store.js +3 -2
  28. package/dist/components/builder/builder-store.js.map +1 -1
  29. package/dist/components/builder/canvas-chrome-css.js +0 -17
  30. package/dist/components/builder/canvas-chrome-css.js.map +1 -1
  31. package/dist/components/builder/canvas-chrome.d.ts +10 -12
  32. package/dist/components/builder/canvas-chrome.js +12 -13
  33. package/dist/components/builder/canvas-chrome.js.map +1 -1
  34. package/dist/components/builder/canvas-dnd.d.ts +8 -0
  35. package/dist/components/builder/canvas-dnd.js +5 -3
  36. package/dist/components/builder/canvas-dnd.js.map +1 -1
  37. package/dist/components/builder/canvas-render-context.d.ts +12 -24
  38. package/dist/components/builder/canvas-render-context.js +15 -44
  39. package/dist/components/builder/canvas-render-context.js.map +1 -1
  40. package/dist/components/builder/canvas.js +5 -11
  41. package/dist/components/builder/canvas.js.map +1 -1
  42. package/dist/components/builder/clipboard-actions.d.ts +14 -6
  43. package/dist/components/builder/clipboard-actions.js +29 -20
  44. package/dist/components/builder/clipboard-actions.js.map +1 -1
  45. package/dist/components/builder/composition-field-editor.d.ts +19 -26
  46. package/dist/components/builder/composition-field-editor.js +30 -52
  47. package/dist/components/builder/composition-field-editor.js.map +1 -1
  48. package/dist/components/builder/insert-context.d.ts +3 -1
  49. package/dist/components/builder/insert-context.js +15 -10
  50. package/dist/components/builder/insert-context.js.map +1 -1
  51. package/dist/components/builder/page-tree.d.ts +29 -0
  52. package/dist/components/builder/page-tree.js +56 -58
  53. package/dist/components/builder/page-tree.js.map +1 -1
  54. package/dist/components/builder/reuse.js +5 -2
  55. package/dist/components/builder/reuse.js.map +1 -1
  56. package/dist/components/cms/asset-field.js +20 -32
  57. package/dist/components/cms/asset-field.js.map +1 -1
  58. package/dist/components/cms/cms-composition-validators.js +11 -2
  59. package/dist/components/cms/cms-composition-validators.js.map +1 -1
  60. package/dist/components/cms/editable-cell.js +5 -2
  61. package/dist/components/cms/editable-cell.js.map +1 -1
  62. package/dist/components/cms/use-document-versions.js +1 -0
  63. package/dist/components/cms/use-document-versions.js.map +1 -1
  64. package/dist/components/cms/versions-model.d.ts +0 -12
  65. package/dist/components/cms/versions-model.js +3 -15
  66. package/dist/components/cms/versions-model.js.map +1 -1
  67. package/dist/components/components/component-detail.js +1 -1
  68. package/dist/components/components/component-detail.js.map +1 -1
  69. package/dist/components/components/transfer-dialog.js +2 -4
  70. package/dist/components/components/transfer-dialog.js.map +1 -1
  71. package/dist/components/components/transfer.d.ts +13 -10
  72. package/dist/components/components/transfer.js +21 -19
  73. package/dist/components/components/transfer.js.map +1 -1
  74. package/dist/components/project-session.d.ts +9 -0
  75. package/dist/components/project-session.js +1 -1
  76. package/dist/components/project-session.js.map +1 -1
  77. package/dist/components/redirects/redirects-collection-view-source.d.ts +17 -2
  78. package/dist/components/redirects/redirects-collection-view-source.js +20 -6
  79. package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
  80. package/dist/components/section-surface.js +3 -2
  81. package/dist/components/section-surface.js.map +1 -1
  82. package/dist/components/settings-form.js +1 -2
  83. package/dist/components/settings-form.js.map +1 -1
  84. package/dist/components/studio-context.js +1 -18
  85. package/dist/components/studio-context.js.map +1 -1
  86. package/dist/components/studio-project-screen.js +3 -5
  87. package/dist/components/studio-project-screen.js.map +1 -1
  88. package/dist/lib/ai/apply-content.js +73 -29
  89. package/dist/lib/ai/apply-content.js.map +1 -1
  90. package/dist/lib/ai/constraints.d.ts +35 -0
  91. package/dist/lib/ai/constraints.js +104 -4
  92. package/dist/lib/ai/constraints.js.map +1 -1
  93. package/dist/lib/ai/route.js +4 -1
  94. package/dist/lib/ai/route.js.map +1 -1
  95. package/dist/lib/ai/shape.d.ts +3 -2
  96. package/dist/lib/ai/shape.js +16 -24
  97. package/dist/lib/ai/shape.js.map +1 -1
  98. package/dist/lib/ai/types.d.ts +8 -0
  99. package/dist/lib/ai/types.js.map +1 -1
  100. package/dist/lib/ai/update-fields.js +33 -12
  101. package/dist/lib/ai/update-fields.js.map +1 -1
  102. package/dist/lib/assets/asset-placements.d.ts +35 -0
  103. package/dist/lib/assets/asset-placements.js +74 -0
  104. package/dist/lib/assets/asset-placements.js.map +1 -0
  105. package/dist/lib/assets/backfill-thumbs.d.ts +9 -8
  106. package/dist/lib/assets/backfill-thumbs.js +13 -25
  107. package/dist/lib/assets/backfill-thumbs.js.map +1 -1
  108. package/dist/lib/clipboard/paste-issues.d.ts +17 -15
  109. package/dist/lib/clipboard/paste-issues.js +8 -45
  110. package/dist/lib/clipboard/paste-issues.js.map +1 -1
  111. package/dist/lib/clipboard/paste-target.d.ts +25 -1
  112. package/dist/lib/clipboard/paste-target.js +13 -6
  113. package/dist/lib/clipboard/paste-target.js.map +1 -1
  114. package/dist/lib/cms/published-documents.js +1 -0
  115. package/dist/lib/cms/published-documents.js.map +1 -1
  116. package/dist/lib/cms-views/view-model.d.ts +3 -3
  117. package/dist/lib/data/index.d.ts +0 -3
  118. package/dist/lib/data/index.js +4 -6
  119. package/dist/lib/data/index.js.map +1 -1
  120. package/dist/lib/dnd/drop-rules.d.ts +45 -2
  121. package/dist/lib/dnd/drop-rules.js +64 -9
  122. package/dist/lib/dnd/drop-rules.js.map +1 -1
  123. package/dist/lib/engine-version.d.ts +1 -1
  124. package/dist/lib/engine-version.js +1 -1
  125. package/dist/lib/engine-version.js.map +1 -1
  126. package/dist/lib/i18n/messages.de.js +4 -56
  127. package/dist/lib/i18n/messages.de.js.map +1 -1
  128. package/dist/lib/i18n/messages.en.d.ts +4 -53
  129. package/dist/lib/i18n/messages.en.js +8 -57
  130. package/dist/lib/i18n/messages.en.js.map +1 -1
  131. package/dist/lib/locale/fallback.js +8 -2
  132. package/dist/lib/locale/fallback.js.map +1 -1
  133. package/dist/lib/locale/index.d.ts +0 -1
  134. package/dist/lib/locale/index.js +0 -1
  135. package/dist/lib/locale/index.js.map +1 -1
  136. package/dist/lib/locale/locale-store.d.ts +10 -3
  137. package/dist/lib/locale/locale-store.js +12 -5
  138. package/dist/lib/locale/locale-store.js.map +1 -1
  139. package/dist/lib/operations/session-hydration.d.ts +9 -5
  140. package/dist/lib/operations/session-hydration.js +10 -10
  141. package/dist/lib/operations/session-hydration.js.map +1 -1
  142. package/dist/lib/persistence/convex-adapter.js +63 -75
  143. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  144. package/dist/lib/persona/labels.d.ts +0 -2
  145. package/dist/lib/persona/labels.js +1 -7
  146. package/dist/lib/persona/labels.js.map +1 -1
  147. package/dist/lib/registry/index.d.ts +4 -2
  148. package/dist/lib/registry/index.js +26 -21
  149. package/dist/lib/registry/index.js.map +1 -1
  150. package/dist/lib/registry/project-components.js +0 -3
  151. package/dist/lib/registry/project-components.js.map +1 -1
  152. package/dist/lib/routes/route-issues.js +4 -5
  153. package/dist/lib/routes/route-issues.js.map +1 -1
  154. package/dist/lib/theme.js +3 -2
  155. package/dist/lib/theme.js.map +1 -1
  156. package/dist/lib/webhooks/tags.js +11 -14
  157. package/dist/lib/webhooks/tags.js.map +1 -1
  158. package/dist/lib/workspace/index.d.ts +1 -1
  159. package/dist/lib/workspace/index.js +1 -1
  160. package/dist/lib/workspace/index.js.map +1 -1
  161. package/dist/previews/enumerate.d.ts +27 -9
  162. package/dist/previews/enumerate.js +27 -6
  163. package/dist/previews/enumerate.js.map +1 -1
  164. package/dist/previews/manifest-format.d.ts +9 -0
  165. package/dist/previews/manifest-format.js.map +1 -1
  166. package/dist/router/match.d.ts +0 -2
  167. package/dist/router/match.js +0 -2
  168. package/dist/router/match.js.map +1 -1
  169. package/dist/ui/badge.d.ts +1 -1
  170. package/package.json +2 -2
  171. package/src/styles/studio.css +5 -4
  172. package/src/styles/tokens.css +72 -55
  173. package/convex/content.ts +0 -291
  174. package/dist/components/collab/collab-context.d.ts +0 -27
  175. package/dist/components/collab/collab-context.js +0 -39
  176. package/dist/components/collab/collab-context.js.map +0 -1
  177. package/dist/components/collab/collab-seed.d.ts +0 -15
  178. package/dist/components/collab/collab-seed.js +0 -20
  179. package/dist/components/collab/collab-seed.js.map +0 -1
  180. package/dist/components/extensions/extensions-view.d.ts +0 -7
  181. package/dist/components/extensions/extensions-view.js +0 -127
  182. package/dist/components/extensions/extensions-view.js.map +0 -1
  183. package/dist/lib/bindings/section-bindings.d.ts +0 -24
  184. package/dist/lib/bindings/section-bindings.js +0 -9
  185. package/dist/lib/bindings/section-bindings.js.map +0 -1
  186. package/dist/lib/data/data-store.d.ts +0 -98
  187. package/dist/lib/data/data-store.js +0 -211
  188. package/dist/lib/data/data-store.js.map +0 -1
  189. package/dist/lib/data/providers.d.ts +0 -143
  190. package/dist/lib/data/providers.js +0 -255
  191. package/dist/lib/data/providers.js.map +0 -1
  192. package/dist/lib/data/sample-capture.d.ts +0 -30
  193. package/dist/lib/data/sample-capture.js +0 -70
  194. package/dist/lib/data/sample-capture.js.map +0 -1
  195. package/dist/lib/data/sources-store.d.ts +0 -42
  196. package/dist/lib/data/sources-store.js +0 -120
  197. package/dist/lib/data/sources-store.js.map +0 -1
  198. package/dist/lib/locale/binding-fallback.d.ts +0 -35
  199. package/dist/lib/locale/binding-fallback.js +0 -73
  200. package/dist/lib/locale/binding-fallback.js.map +0 -1
  201. package/dist/lib/starters/kit-session.d.ts +0 -5
  202. package/dist/lib/starters/kit-session.js +0 -17
  203. package/dist/lib/starters/kit-session.js.map +0 -1
@@ -0,0 +1,180 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { convexTest } from 'convex-test'
3
+ import { makeFunctionReference } from 'convex/server'
4
+ import type { ConvexHttpClient } from 'convex/browser'
5
+ import type { FunctionReference } from 'convex/server'
6
+ import { InMemoryPersistenceAdapter } from '@elytracms/core/persistence'
7
+ import type { PersistenceAdapter } from '@elytracms/core/persistence'
8
+ import { ConvexPersistenceAdapter } from '../src/lib/persistence/convex-adapter'
9
+ import { updateAssetMetadata } from '../src/components/assets/asset-metadata'
10
+ import { runVariantBackfill } from '../src/lib/assets/backfill-thumbs'
11
+ import schema from './schema'
12
+
13
+ /**
14
+ * EC-577 — the studio's REAL asset writers against BOTH adapters.
15
+ *
16
+ * The contract scenarios (`contract-scenarios.ts`, run by `contract-parity.test.ts`)
17
+ * pin the adapter write rule itself. This file pins the writers on top of it —
18
+ * the metadata dialog's `updateAssetMetadata` and the "Generate previews"
19
+ * backfill — because the reported loss happened between the two: the writer
20
+ * expressed a clear as an omitted key and restated everything else, and the two
21
+ * adapters read that one write in opposite ways. Measured on 162174ce with the
22
+ * same writer calls:
23
+ *
24
+ * in-memory: {"focalPoint":null,"alt":null,"thumb":null,"lqip":null}
25
+ * convex : {"focalPoint":{"x":0.2,"y":0.8},"alt":"Harbour","thumb":{…},"lqip":"data:…"}
26
+ *
27
+ * i.e. the playground lost the previews and connected mode never cleared.
28
+ */
29
+
30
+ const modules = {
31
+ ...import.meta.glob(['./**/*.ts', '!./**/*.test.ts', '!./_generated/**']),
32
+ './_generated/api.ts': async () => ({}),
33
+ }
34
+
35
+ const PROJECT_ID = 'rail-write-parity'
36
+ const SESSION_USER_ID = 'user_admin'
37
+
38
+ async function convexBackend() {
39
+ const t = convexTest(schema, modules)
40
+ const now = new Date().toISOString()
41
+ await t.run(async (ctx) => {
42
+ await ctx.db.insert('members', { userId: SESSION_USER_ID, role: 'admin', createdAt: now, updatedAt: now })
43
+ })
44
+ const asAdmin = t.withIdentity({ subject: SESSION_USER_ID, issuer: 'https://convex.test' })
45
+ const client = {
46
+ query: (ref: FunctionReference<'query'>, args?: Record<string, unknown>) => asAdmin.query(ref, args ?? {}),
47
+ mutation: (ref: FunctionReference<'mutation'>, args?: Record<string, unknown>) => asAdmin.mutation(ref, args ?? {}),
48
+ } as unknown as ConvexHttpClient
49
+ return { t, adapter: new ConvexPersistenceAdapter(client, PROJECT_ID, []) }
50
+ }
51
+
52
+ const THUMB = { storageKey: 'blob_thumb', url: 'https://cdn.test/t.webp', width: 480, height: 240 }
53
+ const LQIP = 'data:image/webp;base64,AAAA'
54
+
55
+ async function seedHero(adapter: PersistenceAdapter) {
56
+ await adapter.assets.upsertAsset(PROJECT_ID, {
57
+ id: 'asset_hero',
58
+ filename: 'hero.jpg',
59
+ contentType: 'image/jpeg',
60
+ byteSize: 10,
61
+ width: 4000,
62
+ height: 2000,
63
+ alt: 'Harbour',
64
+ title: 'Hero',
65
+ focalPoint: { x: 0.2, y: 0.8 },
66
+ thumb: THUMB,
67
+ lqip: LQIP,
68
+ meta: { alt: 'Harbour', legacyKey: 'kept' },
69
+ })
70
+ }
71
+
72
+ /** "Reset to centre", then clear alt — the two connected clears that never persisted. */
73
+ async function metadataClears(adapter: PersistenceAdapter) {
74
+ await seedHero(adapter)
75
+ const assets = adapter.assets
76
+ const reset = await updateAssetMetadata({ assets }, PROJECT_ID, 'asset_hero', { focalPoint: null })
77
+ const clearedAlt = await updateAssetMetadata({ assets }, PROJECT_ID, 'asset_hero', { alt: '' })
78
+ expect([reset.ok, clearedAlt.ok]).toEqual([true, true])
79
+ const after = await adapter.assets.getAsset(PROJECT_ID, 'asset_hero')
80
+ return {
81
+ focalPoint: after.focalPoint ?? null,
82
+ alt: after.alt ?? null,
83
+ title: after.title ?? null,
84
+ thumb: after.thumb ?? null,
85
+ lqip: after.lqip ?? null,
86
+ meta: after.meta ?? null,
87
+ }
88
+ }
89
+
90
+ describe('EC-577 — the asset writers mean the same thing on both adapters', () => {
91
+ it('a metadata clear clears, and keeps thumb/lqip/meta, in the playground AND connected', async () => {
92
+ const memory = await metadataClears(new InMemoryPersistenceAdapter())
93
+ const convex = await metadataClears((await convexBackend()).adapter)
94
+ const expected = {
95
+ focalPoint: null,
96
+ alt: null,
97
+ title: 'Hero',
98
+ thumb: THUMB,
99
+ lqip: LQIP,
100
+ meta: { alt: 'Harbour', legacyKey: 'kept' },
101
+ }
102
+ expect({ memory, convex }).toEqual({ memory: expected, convex: expected })
103
+ })
104
+
105
+ it('a metadata write to a deleted asset is refused, never re-creates it', async () => {
106
+ for (const adapter of [new InMemoryPersistenceAdapter(), (await convexBackend()).adapter]) {
107
+ const result = await updateAssetMetadata({ assets: adapter.assets }, PROJECT_ID, 'asset_gone', {
108
+ focalPoint: { x: 0.5, y: 0.5 },
109
+ })
110
+ expect(result.ok).toBe(false)
111
+ if (!result.ok) expect(result.message).toContain('No asset “asset_gone” is stored')
112
+ expect(await adapter.assets.findAsset(PROJECT_ID, 'asset_gone')).toBeUndefined()
113
+ }
114
+ })
115
+
116
+ it('the preview backfill writes only the variants: an alt saved mid-run survives', async () => {
117
+ for (const adapter of [new InMemoryPersistenceAdapter(), (await convexBackend()).adapter]) {
118
+ await adapter.assets.upsertAsset(PROJECT_ID, {
119
+ id: 'asset_big',
120
+ filename: 'big.jpg',
121
+ contentType: 'image/jpeg',
122
+ byteSize: 10,
123
+ width: 4000,
124
+ height: 3000,
125
+ storageKey: 'blob_big',
126
+ })
127
+ const blobs = {
128
+ async get() {
129
+ // An editor saves alt while the backfill is fetching this original — the
130
+ // backfill's write lands after it, from a listing taken before it.
131
+ await updateAssetMetadata({ assets: adapter.assets }, PROJECT_ID, 'asset_big', { alt: 'Mid-run alt' })
132
+ return new Uint8Array([1])
133
+ },
134
+ }
135
+ const progress = await runVariantBackfill(
136
+ {
137
+ assets: adapter.assets,
138
+ blobs: blobs as never,
139
+ generate: async () => ({ thumb: THUMB, lqip: LQIP }),
140
+ },
141
+ PROJECT_ID,
142
+ )
143
+ expect(progress.generated).toBe(1)
144
+ const after = await adapter.assets.getAsset(PROJECT_ID, 'asset_big')
145
+ expect([after.alt, after.thumb, after.lqip]).toEqual(['Mid-run alt', THUMB, LQIP])
146
+ }
147
+ })
148
+ })
149
+
150
+ describe('EC-577 (D13/F24) — the one asset row → record projection on the Convex delivery route', () => {
151
+ // EC-574 deleted the second route (the `createContent` kit); the snapshot is the
152
+ // remaining reader of the projection, and this pins the delivery cut it serves.
153
+ it('delivery:snapshot delivers the asset with its lqip and without editorial columns', async () => {
154
+ const { t, adapter } = await convexBackend()
155
+ await adapter.assets.upsertAsset(PROJECT_ID, {
156
+ id: 'asset_p',
157
+ filename: 'p.png',
158
+ contentType: 'image/png',
159
+ byteSize: 1,
160
+ url: 'https://assets.test/p.png',
161
+ title: 'Library title',
162
+ thumb: THUMB,
163
+ lqip: LQIP,
164
+ })
165
+ await adapter.cms.upsertDocument(PROJECT_ID, {
166
+ collection: 'post',
167
+ id: 'one',
168
+ values: { cover: { type: 'asset', id: 'asset_p' } },
169
+ })
170
+ const snapshot = (await t.query(makeFunctionReference<'query'>('delivery:snapshot'), {
171
+ projectId: PROJECT_ID,
172
+ })) as { assets: Record<string, unknown>[] }
173
+ // The snapshot is the positive control: it always carried the lqip.
174
+ expect(snapshot.assets[0]?.lqip).toBe(LQIP)
175
+ // Editorial-only columns stay off the delivery wire (EC-400 thumb; the library title).
176
+ expect(snapshot.assets[0]).not.toHaveProperty('thumb')
177
+ expect(snapshot.assets[0]).not.toHaveProperty('title')
178
+ expect(snapshot.assets[0]).toMatchObject({ url: 'https://assets.test/p.png' })
179
+ })
180
+ })
@@ -1,7 +1,12 @@
1
- import { v } from 'convex/values'
1
+ import { ConvexError, v } from 'convex/values'
2
+ import type { DocumentByName, WithoutSystemFields } from 'convex/server'
3
+ import { redirectRowSchema, rejectedWriteMessage, resolveRecordWrite } from '@elytracms/core/persistence'
4
+ import type { DataModel } from './functions'
2
5
  import { mutation, query } from './functions'
3
6
  import { requireCapability } from './guard'
4
7
 
8
+ type RedirectDoc = DocumentByName<DataModel, 'redirects'>
9
+
5
10
  /**
6
11
  * Editorial redirects persistence (EC-303). Redirects are data an editor owns —
7
12
  * saved+enabled entries go live dynamically (no deploy) via the delivery
@@ -10,30 +15,47 @@ import { requireCapability } from './guard'
10
15
  * are content, not administration.
11
16
  */
12
17
 
18
+ /**
19
+ * Create or modify a redirect (EC-577). The ONE write rule (`resolveRecordWrite`,
20
+ * shared with the in-memory adapter) runs here, inside the transaction: a column
21
+ * absent from the args keeps its stored value — `permanent`/`enabled` included,
22
+ * whose defaults (the record schema's `.default(true)`) apply only when this save
23
+ * creates the redirect — `null` clears the optional `meta` bag, a value sets. ONE
24
+ * `db.patch` or ONE `db.insert`, so the EC-510 revalidation trigger fires once.
25
+ */
13
26
  export const save = mutation({
14
27
  args: {
15
28
  redirectId: v.string(),
16
- from: v.string(),
17
- to: v.string(),
18
- permanent: v.boolean(),
19
- enabled: v.boolean(),
20
- // Schema-driven additive-field bag (EC-311 Tier 2). Absent on a save leaves the
21
- // stored bag untouched (Convex `patch` only writes provided fields).
29
+ from: v.optional(v.string()),
30
+ to: v.optional(v.string()),
31
+ permanent: v.optional(v.boolean()),
32
+ enabled: v.optional(v.boolean()),
33
+ // Schema-driven additive-field bag (EC-311 Tier 2): present replaces the bag,
34
+ // `null` clears it, absent keeps it.
22
35
  meta: v.optional(v.any()),
23
36
  createdAt: v.string(),
24
37
  updatedAt: v.string(),
25
38
  },
26
- handler: async (ctx, args) => {
39
+ handler: async (ctx, { redirectId, createdAt, updatedAt, ...write }) => {
27
40
  await requireCapability(ctx, 'content.write')
28
41
  const existing = await ctx.db
29
42
  .query('redirects')
30
- .withIndex('by_redirect', (q) => q.eq('redirectId', args.redirectId))
43
+ .withIndex('by_redirect', (q) => q.eq('redirectId', redirectId))
31
44
  .first()
45
+ const resolved = resolveRecordWrite(redirectRowSchema, existing ?? undefined, write)
46
+ if (!resolved.ok) {
47
+ throw new ConvexError({
48
+ code: 'validation',
49
+ message: rejectedWriteMessage('redirect', redirectId, resolved.issues),
50
+ issues: resolved.issues,
51
+ })
52
+ }
32
53
  if (existing) {
33
- await ctx.db.patch(existing._id, { ...args, createdAt: existing.createdAt })
54
+ await ctx.db.patch(existing._id, { ...(resolved.patch as Partial<RedirectDoc>), updatedAt })
34
55
  return ctx.db.get(existing._id)
35
56
  }
36
- const id = await ctx.db.insert('redirects', args)
57
+ const row: WithoutSystemFields<RedirectDoc> = { ...resolved.row, redirectId, createdAt, updatedAt }
58
+ const id = await ctx.db.insert('redirects', row)
37
59
  return ctx.db.get(id)
38
60
  },
39
61
  })
@@ -19,17 +19,19 @@ const webhooksDeliver = makeFunctionReference<'action'>('webhooks:deliver')
19
19
  * var. An unconfigured deployment (local dev, no env) is a silent no-op —
20
20
  * publishing must never depend on a reachable host.
21
21
  *
22
- * Called from the EC-286 write-chokepoint triggers only (`triggers.ts`), so
23
- * every writer of a live-wire table is covered by construction: `cmsDocuments`
24
- * when the published pin moves, and `redirects` on any write (EC-510 — a
25
- * saved+enabled redirect IS the live state, with no draft/published split).
22
+ * Called from the EC-286 write-chokepoint triggers only, so every writer of a
23
+ * live-wire table is covered by construction. Which tables, and which of their
24
+ * writes, is declared once — in `triggers.ts`, not here.
26
25
  *
27
26
  * Doc/collection-scoped tags (matching the client dispatcher's
28
27
  * `webhookTagsForEvent`) would be more surgical for DOCUMENTS, but need the
29
28
  * project locales — not available in the Convex env here. The sweep is the
30
29
  * honest, reliable v1; threading scoped tags is a precision follow-up. For
31
30
  * redirects the sweep is not a compromise but the correct answer — see
32
- * `ROUTING_OPERATIONS` in `src/lib/webhooks/tags.ts`.
31
+ * `ROUTING_OPERATIONS` in `src/lib/webhooks/tags.ts`. For assets it is the only
32
+ * correct answer today: a page reading an asset through a document asset field
33
+ * carries no `asset:<id>` tag (EC-566 D16), so the per-asset tag the browser
34
+ * dispatcher emits would miss it.
33
35
  */
34
36
  export async function scheduleRevalidate(ctx: MutationCtx, event: string): Promise<void> {
35
37
  const env =
@@ -23,9 +23,22 @@ import { scheduleRevalidate } from './revalidate'
23
23
  *
24
24
  * EC-287 retired the DB-resident project graph and its publish path, so
25
25
  * `cmsDocuments` is the SINGLE document trigger — one publish path, the
26
- * dual-path footgun gone at the source. EC-510 added `redirects`, the other
27
- * table whose rows go straight onto the live wire; both are registered here so
28
- * "which writes reach the host" has one place to read.
26
+ * dual-path footgun gone at the source.
27
+ *
28
+ * **Which writes reach the host** — every table the delivery snapshot
29
+ * (`delivery.ts`) projects onto the live wire is registered here, and nowhere
30
+ * else decides it:
31
+ *
32
+ * - `cmsDocuments` — only when the live pin (`publishedVersion`) moves;
33
+ * - `redirects` (EC-510) — every write (the row IS the live state);
34
+ * - `assets` (EC-579) — every edit and delete; a create fires nothing.
35
+ *
36
+ * The snapshot reads one more table, `documentVersions`, but only the version a
37
+ * `cmsDocuments` row pins — version rows are insert-only (`cms.ts`) and deleted
38
+ * only with their document, so they reach the wire through a pin move and need
39
+ * no trigger of their own. The rest (`members`, `invites`, `authorityAudit`,
40
+ * `referenceEntries`, `referenceIndexMarker`, the auth tables) never reach the
41
+ * wire. A table the snapshot starts projecting belongs in the list above.
29
42
  */
30
43
  export const triggers = new Triggers<DataModel>()
31
44
 
@@ -92,3 +105,40 @@ triggers.register('cmsDocuments', async (ctx, change) => {
92
105
  triggers.register('redirects', async (ctx, change) => {
93
106
  await scheduleRevalidate(ctx, change.operation === 'delete' ? 'redirects.remove' : 'redirects.save')
94
107
  })
108
+
109
+ /**
110
+ * Asset records (EC-579). The delivery snapshot projects every asset row onto
111
+ * the live wire field by field (url, dimensions, alt, focal point, lqip, the
112
+ * `meta` bag), and like a redirect there is no draft/published split: an alt,
113
+ * focal-point or re-upload edit IS live output the moment it commits. Before
114
+ * this, the only emitter for an asset edit was the studio's browser dispatcher
115
+ * (`src/lib/webhooks`, active only when the studio bundle carries
116
+ * `VITE_REVALIDATE_*`), so a deployment revalidating through this server path
117
+ * alone — and any writer other than that browser — changed the live site
118
+ * without the host ever hearing of it (EC-566 M5; measured by the Codex
119
+ * review, P07).
120
+ *
121
+ * - **Update / delete → the whole-project sweep.** Not `asset:<id>`: the host
122
+ * stamps that tag only on assets a page bakes through its composition, rich
123
+ * text or OG image (`scopeAssetsToPage` in `@elytracms/next` `route-core.ts`);
124
+ * an asset read through a document asset FIELD puts no tag on the page, so a
125
+ * per-asset tag would leave exactly those pages stale (EC-566 D16 / Codex F14).
126
+ * The sweep tag is on every cache entry, so it is correct by construction.
127
+ * - **Insert → nothing.** A brand-new asset id appears on no page yet — the
128
+ * same rule the browser dispatcher applies to an `assets.upsert` create.
129
+ *
130
+ * Every update fires, including one that changed nothing on the wire (the
131
+ * editorial-only `thumb` variant, or a no-op re-save): the trigger cannot
132
+ * cheaply prove the delivery projection unchanged, and deciding it here would
133
+ * be a second declaration of which asset fields ride the wire (`delivery.ts`
134
+ * owns that). Asset saves are explicit actions — the detail surface's Save,
135
+ * the options dialog's close, a focal-point release — not keystrokes; the one
136
+ * bulk writer is the rail's "Generate previews" backfill, one sweep per asset.
137
+ *
138
+ * The event names are the operation names (`assets.upsert` / `assets.remove`),
139
+ * matching what the browser dispatcher carries for the same writes.
140
+ */
141
+ triggers.register('assets', async (ctx, change) => {
142
+ if (change.operation === 'insert') return
143
+ await scheduleRevalidate(ctx, change.operation === 'delete' ? 'assets.remove' : 'assets.upsert')
144
+ })
@@ -2,7 +2,6 @@ import type { ReactNode } from 'react';
2
2
  import type { LucideIcon } from 'lucide-react';
3
3
  import type { CmsDocument, CollectionDef, CollectionFieldOverride, CollectionFormMeta, LocaleConfig } from '@elytracms/core/cms-core';
4
4
  import type { CustomizationIssue, Project, SectionId } from '@elytracms/core/studio-core';
5
- import type { InstantiatedKit } from '@elytracms/core/starter-kits';
6
5
  import type { HydratedCms } from '@elytracms/studio/lib/operations';
7
6
  import { type AppViewContext, type AppViewDeepLink, type AppViewModule, type NavPermission, type SidebarViewNode } from './module.js';
8
7
  /**
@@ -81,7 +80,7 @@ export interface RedirectsViewConfig extends BuiltinViewConfig {
81
80
  * different in kind: redirects' base is an engine-owned, fixed `CollectionDef`
82
81
  * known at config-build time; the assets base is the DEPLOYMENT's own
83
82
  * `kind:'asset'` collection (a project's `cms/collections/asset.ts`), only
84
- * knowable once the deployment resolves (hydrated session → kit → playground
83
+ * knowable once the deployment resolves (hydrated session → playground
85
84
  * config — see {@link resolveAssetsSchema}). So the merge runs at RENDER time
86
85
  * (inside `assetsView`'s `CmsSessionGate`), not at `nav.assets(config)` call
87
86
  * time: a locked-violation override (e.g. changing a declared field's type)
@@ -162,7 +161,7 @@ export declare function componentsView(config?: BuiltinViewConfig): AppViewModul
162
161
  export declare function redirectsView(config?: RedirectsViewConfig): AppViewModule;
163
162
  /**
164
163
  * Resolve the assets rail's EFFECTIVE schema (EC-319/EC-311): the deployment's
165
- * OWN `kind:'asset'` collection — hydrated session → kit → playground config,
164
+ * OWN `kind:'asset'` collection — hydrated session → playground config,
166
165
  * the exact same precedence {@link resolveCmsInitialCollections} gives every
167
166
  * other CMS collection — falling back to the engine's `assetCollection` fixture
168
167
  * only when nothing resolves (mirroring `CmsProvider`'s own fallback). A mount's
@@ -188,8 +187,6 @@ export declare function assetsView(config?: AssetsViewConfig): AppViewModule;
188
187
  * admin/config concern, so it appears/hides with a project's settings surface).
189
188
  */
190
189
  export declare function usersView(config?: BuiltinViewConfig): AppViewModule;
191
- /** The extensions marketplace view. */
192
- export declare function extensionsView(config?: BuiltinViewConfig): AppViewModule;
193
190
  /** The project settings view. */
194
191
  export declare function settingsView(config?: BuiltinViewConfig): AppViewModule;
195
192
  /** The legacy `builder` redirect — a hidden, routable view (so `/builder?page=` still lands). */
@@ -274,8 +271,6 @@ export declare const nav: {
274
271
  readonly redirects: (config?: RedirectsViewConfig) => SidebarViewNode;
275
272
  /** Project settings. */
276
273
  readonly settings: (config?: BuiltinViewConfig) => SidebarViewNode;
277
- /** The extensions marketplace. */
278
- readonly extensions: (config?: BuiltinViewConfig) => SidebarViewNode;
279
274
  /** The admin users roster (EC-240). */
280
275
  readonly users: (config?: BuiltinViewConfig) => SidebarViewNode;
281
276
  /** A fully custom view — pass your own `render` (and optional `parsePath`). */
@@ -302,11 +297,12 @@ export declare const nav: {
302
297
  * section rail reads `resolveDeploymentCollections()` directly, and now so does
303
298
  * the store's last rung, so the two can no longer disagree.
304
299
  */
305
- export declare function resolveCmsInitialCollections(cms: HydratedCms | null, kit: InstantiatedKit | undefined): CollectionDef[] | undefined;
300
+ export declare function resolveCmsInitialCollections(cms: HydratedCms | null): CollectionDef[] | undefined;
306
301
  /**
307
302
  * The CMS documents source for a project mount — the document-side twin of
308
- * {@link resolveCmsInitialCollections}: a hydrated session wins (Convex), then a
309
- * kit's imported documents, then **nothing**.
303
+ * {@link resolveCmsInitialCollections}: a hydrated session wins (Convex), then
304
+ * **nothing**. (The middle rung, a starter kit's imported documents, went with
305
+ * the kits — EC-574; K rebuilds starter kits as seeds, EC-200/EC-475.)
310
306
  *
311
307
  * EC-479 — the third rung used to be the engine's own `sampleDocuments` fixture,
312
308
  * reconciled against the project's schema (EC-424). Reconciling it was already an
@@ -323,7 +319,7 @@ export declare function resolveCmsInitialCollections(cms: HydratedCms | null, ki
323
319
  * adapter seeds is `persistence/seed.ts`'s business — EC-475 (project-defined seed
324
320
  * content) owns it.
325
321
  */
326
- export declare function resolveCmsInitialDocuments(cms: HydratedCms | null, kit: InstantiatedKit | undefined): CmsDocument[] | undefined;
322
+ export declare function resolveCmsInitialDocuments(cms: HydratedCms | null): CmsDocument[] | undefined;
327
323
  /**
328
324
  * The CMS locale config for a project mount (EC-357 finding #3). Locales are
329
325
  * structure and structure is code: when the active `elytra.config.ts` project
@@ -334,13 +330,17 @@ export declare function resolveCmsInitialDocuments(cms: HydratedCms | null, kit:
334
330
  * showed `EN·/DE` against a `locales: ['de']` config.
335
331
  *
336
332
  * EC-479/EC-483 — the ladder is now TOTAL, and every rung is the project's own:
337
- * config-declared locales, then (hydrated) its persisted settings, then a kit's,
338
- * then its settings regardless. A `Project` only ever exists for a config-declared
339
- * project (`studio-context.tsx`: `configProjects()`), so `project.settings` is
340
- * config-derived too — which is what lets the CMS session require a locale instead
341
- * of borrowing the engine's `en/de` fixture. The default locale is the literal
342
- * storage key for every localized value, so a guessed one is a data migration.
343
- * `configLocales` is a parameter purely so the ladder is unit-testable (same seam
344
- * pattern as {@link resolveCmsInitialCollections}).
333
+ * config-declared locales, then its settings (a starter kit's locales used to sit
334
+ * between the two; the kits went in EC-574). A `Project` only ever exists for a
335
+ * config-declared project (`studio-context.tsx`: `configProjects()`), so
336
+ * `project.settings` is config-derived too — which is what lets the CMS session
337
+ * require a locale instead of borrowing the engine's `en/de` fixture. The default
338
+ * locale is the literal storage key for every localized value, so a guessed one is
339
+ * a data migration. Both rungs are config-derived and pass the workspace gate
340
+ * (`default-locale-not-in-locales` is an error there), so the value is valid by
341
+ * construction and no reader repairs it. `configLocales` is a parameter purely so
342
+ * the ladder is unit-testable (same seam pattern as
343
+ * {@link resolveCmsInitialCollections}); `cms` is accepted for signature parity
344
+ * with its twins but no longer selects a rung.
345
345
  */
346
- export declare function resolveCmsInitialLocaleConfig(cms: HydratedCms | null, kit: InstantiatedKit | undefined, project: Project, configLocales?: LocaleConfig | undefined): LocaleConfig;
346
+ export declare function resolveCmsInitialLocaleConfig(_cms: HydratedCms | null, project: Project, configLocales?: LocaleConfig | undefined): LocaleConfig;
@@ -1,11 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect } from 'react';
3
- import { Blocks, Database, FileCog, Image, LayoutGrid, Puzzle, Settings as SettingsIcon, Signpost, Users, } from 'lucide-react';
3
+ import { Blocks, Database, FileCog, Image, LayoutGrid, Settings as SettingsIcon, Signpost, Users, } from 'lucide-react';
4
4
  import { assetCollection, extendCollectionDef, isSingleton, resolveSingletonDocument, } from '@elytracms/core/cms-core';
5
5
  import { NavigateOnce, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router';
6
6
  import { Text } from '@elytracms/studio/ui';
7
7
  import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
8
- import { getInstantiatedKit } from '@elytracms/studio/lib/starters/kit-session';
9
8
  import { resolveDeploymentCollections, resolveDeploymentLocaleConfig } from '@elytracms/studio/lib/workspace';
10
9
  import { cmsDeepLink, componentsDeepLink, usersDeepLink, redirectsDeepLink, assetsDeepLink, groupNode, separatorNode, viewNode, } from './module.js';
11
10
  import { SectionSurface } from '../section-surface.js';
@@ -15,7 +14,6 @@ import { AssetsModule } from '../assets/assets-module.js';
15
14
  import { UsersModule } from '../users/users-module.js';
16
15
  import { RedirectsModule } from '../redirects/redirects-module.js';
17
16
  import { redirectsCollectionDef } from '../redirects/redirects-schema.js';
18
- import { ExtensionsView } from '../extensions/extensions-view.js';
19
17
  import { RegistryBrowser } from '../components/registry-browser.js';
20
18
  import { ComponentsIndex } from '../components/components-index.js';
21
19
  import { ComponentDetail } from '../components/component-detail.js';
@@ -210,7 +208,7 @@ export function redirectsView(config = {}) {
210
208
  }
211
209
  /**
212
210
  * Resolve the assets rail's EFFECTIVE schema (EC-319/EC-311): the deployment's
213
- * OWN `kind:'asset'` collection — hydrated session → kit → playground config,
211
+ * OWN `kind:'asset'` collection — hydrated session → playground config,
214
212
  * the exact same precedence {@link resolveCmsInitialCollections} gives every
215
213
  * other CMS collection — falling back to the engine's `assetCollection` fixture
216
214
  * only when nothing resolves (mirroring `CmsProvider`'s own fallback). A mount's
@@ -243,8 +241,7 @@ export function assetsView(config = {}) {
243
241
  availabilitySection: 'assets',
244
242
  parsePath: assetsDeepLink,
245
243
  render: (ctx) => (_jsx(SectionSurface, { project: ctx.project, section: "assets", forcedState: ctx.search.state, children: ctx.deepLink.assetId === null && ctx.search.asset ? (_jsx(NavigateOnce, { to: STUDIO_PROJECT_ROUTE, params: { projectId: ctx.projectId, _splat: studioPath.asset(ctx.search.asset) }, replace: true })) : (_jsx(CmsSessionGate, { children: (cms) => {
246
- const kit = getInstantiatedKit(ctx.projectId);
247
- const schema = resolveAssetsSchema(resolveCmsInitialCollections(cms, kit), config);
244
+ const schema = resolveAssetsSchema(resolveCmsInitialCollections(cms), config);
248
245
  return (_jsx(AssetsModule, { projectId: ctx.projectId, assetId: ctx.deepLink.assetId ?? null, schema: schema }));
249
246
  } })) })),
250
247
  };
@@ -273,21 +270,6 @@ export function usersView(config = {}) {
273
270
  render: (ctx) => ctx.search.state && ctx.search.state !== 'working' ? (_jsx(SectionStateView, { state: ctx.search.state, title: translate('nav.users.label') })) : (_jsx(UsersModule, { projectId: ctx.projectId, userId: ctx.deepLink.userId ?? null })),
274
271
  };
275
272
  }
276
- /** The extensions marketplace view. */
277
- export function extensionsView(config = {}) {
278
- return {
279
- id: 'extensions',
280
- get label() {
281
- return config.label ?? translate('nav.extensions.label');
282
- },
283
- get description() {
284
- return translate('nav.extensions.description');
285
- },
286
- icon: config.icon ?? Puzzle,
287
- availabilitySection: 'extensions',
288
- render: (ctx) => ctx.search.state && ctx.search.state !== 'working' ? (_jsx(SectionStateView, { state: ctx.search.state, title: translate('nav.extensions.label') })) : (_jsx(ExtensionsView, {})),
289
- };
290
- }
291
273
  /** The project settings view. */
292
274
  export function settingsView(config = {}) {
293
275
  return {
@@ -355,7 +337,6 @@ export const DEFAULT_VIEWS = [
355
337
  cmsView(),
356
338
  redirectsView(),
357
339
  assetsView(),
358
- extensionsView(),
359
340
  usersView(),
360
341
  settingsView(),
361
342
  builderView(),
@@ -505,8 +486,6 @@ export const nav = {
505
486
  redirects: (config = {}) => viewNode(withNavPermission(redirectsView(config), config)),
506
487
  /** Project settings. */
507
488
  settings: (config = {}) => viewNode(withNavPermission(settingsView(config), config)),
508
- /** The extensions marketplace. */
509
- extensions: (config = {}) => viewNode(withNavPermission(extensionsView(config), config)),
510
489
  /** The admin users roster (EC-240). */
511
490
  users: (config = {}) => viewNode(withNavPermission(usersView(config), config)),
512
491
  /** A fully custom view — pass your own `render` (and optional `parsePath`). */
@@ -545,13 +524,14 @@ function ComponentsScreen({ projectId, componentId, state, }) {
545
524
  * section rail reads `resolveDeploymentCollections()` directly, and now so does
546
525
  * the store's last rung, so the two can no longer disagree.
547
526
  */
548
- export function resolveCmsInitialCollections(cms, kit) {
549
- return cms?.collections ?? kit?.collections ?? resolveDeploymentCollections();
527
+ export function resolveCmsInitialCollections(cms) {
528
+ return cms?.collections ?? resolveDeploymentCollections();
550
529
  }
551
530
  /**
552
531
  * The CMS documents source for a project mount — the document-side twin of
553
- * {@link resolveCmsInitialCollections}: a hydrated session wins (Convex), then a
554
- * kit's imported documents, then **nothing**.
532
+ * {@link resolveCmsInitialCollections}: a hydrated session wins (Convex), then
533
+ * **nothing**. (The middle rung, a starter kit's imported documents, went with
534
+ * the kits — EC-574; K rebuilds starter kits as seeds, EC-200/EC-475.)
555
535
  *
556
536
  * EC-479 — the third rung used to be the engine's own `sampleDocuments` fixture,
557
537
  * reconciled against the project's schema (EC-424). Reconciling it was already an
@@ -568,8 +548,8 @@ export function resolveCmsInitialCollections(cms, kit) {
568
548
  * adapter seeds is `persistence/seed.ts`'s business — EC-475 (project-defined seed
569
549
  * content) owns it.
570
550
  */
571
- export function resolveCmsInitialDocuments(cms, kit) {
572
- return cms?.documents ?? kit?.documents;
551
+ export function resolveCmsInitialDocuments(cms) {
552
+ return cms?.documents;
573
553
  }
574
554
  /**
575
555
  * The CMS locale config for a project mount (EC-357 finding #3). Locales are
@@ -581,25 +561,26 @@ export function resolveCmsInitialDocuments(cms, kit) {
581
561
  * showed `EN·/DE` against a `locales: ['de']` config.
582
562
  *
583
563
  * EC-479/EC-483 — the ladder is now TOTAL, and every rung is the project's own:
584
- * config-declared locales, then (hydrated) its persisted settings, then a kit's,
585
- * then its settings regardless. A `Project` only ever exists for a config-declared
586
- * project (`studio-context.tsx`: `configProjects()`), so `project.settings` is
587
- * config-derived too — which is what lets the CMS session require a locale instead
588
- * of borrowing the engine's `en/de` fixture. The default locale is the literal
589
- * storage key for every localized value, so a guessed one is a data migration.
590
- * `configLocales` is a parameter purely so the ladder is unit-testable (same seam
591
- * pattern as {@link resolveCmsInitialCollections}).
564
+ * config-declared locales, then its settings (a starter kit's locales used to sit
565
+ * between the two; the kits went in EC-574). A `Project` only ever exists for a
566
+ * config-declared project (`studio-context.tsx`: `configProjects()`), so
567
+ * `project.settings` is config-derived too — which is what lets the CMS session
568
+ * require a locale instead of borrowing the engine's `en/de` fixture. The default
569
+ * locale is the literal storage key for every localized value, so a guessed one is
570
+ * a data migration. Both rungs are config-derived and pass the workspace gate
571
+ * (`default-locale-not-in-locales` is an error there), so the value is valid by
572
+ * construction and no reader repairs it. `configLocales` is a parameter purely so
573
+ * the ladder is unit-testable (same seam pattern as
574
+ * {@link resolveCmsInitialCollections}); `cms` is accepted for signature parity
575
+ * with its twins but no longer selects a rung.
592
576
  */
593
- export function resolveCmsInitialLocaleConfig(cms, kit, project, configLocales = resolveDeploymentLocaleConfig()) {
594
- const projectSettings = {
577
+ export function resolveCmsInitialLocaleConfig(_cms, project, configLocales = resolveDeploymentLocaleConfig()) {
578
+ if (configLocales)
579
+ return configLocales;
580
+ return {
595
581
  default: project.settings.defaultLocale,
596
582
  locales: [...project.settings.locales],
597
583
  };
598
- if (configLocales)
599
- return configLocales;
600
- if (cms)
601
- return projectSettings;
602
- return kit?.localeConfig ?? projectSettings;
603
584
  }
604
585
  /**
605
586
  * CMS section: hydrates the persisted (or kit-seeded) collections + documents, then
@@ -614,10 +595,8 @@ function CmsScreen({ project, collectionId, documentId, state, field, focusColle
614
595
  if (state && state !== 'working') {
615
596
  return _jsx(SectionStateView, { state: state, title: translate('nav.cms.label') });
616
597
  }
617
- // A project created from a starter kit (EC-119) opens the CMS on the kit's schemas + content.
618
- const kit = getInstantiatedKit(projectId);
619
598
  return (_jsx(CmsSessionGate, { children: (cms) => {
620
- return (_jsx(CmsProvider, { projectId: projectId, initialCollections: resolveCmsInitialCollections(cms, kit), initialDocuments: resolveCmsInitialDocuments(cms, kit), initialPublishedKeys: cms?.publishedKeys, initialTimestamps: cms?.timestamps, initialPublishTimestamps: cms?.publishTimestamps, initialChangedKeys: cms?.changedKeys, initialLocaleConfig: resolveCmsInitialLocaleConfig(cms, kit, project),
599
+ return (_jsx(CmsProvider, { projectId: projectId, initialCollections: resolveCmsInitialCollections(cms), initialDocuments: resolveCmsInitialDocuments(cms), initialPublishedKeys: cms?.publishedKeys, initialTimestamps: cms?.timestamps, initialPublishTimestamps: cms?.publishTimestamps, initialChangedKeys: cms?.changedKeys, initialLocaleConfig: resolveCmsInitialLocaleConfig(cms, project),
621
600
  // EC-461: a hydrated mount (cms non-null — both modes now) seeds the
622
601
  // CURRENT persisted state, so staged docs are superseded by it; only
623
602
  // a static kit/fixture seed lets staged entries win.