@elytracms/studio 0.0.85 → 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 (248) 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 +54 -116
  5. package/convex/engine-version.test.ts +31 -0
  6. package/convex/functions.ts +3 -3
  7. package/convex/rail-write-parity.test.ts +180 -0
  8. package/convex/redirects.ts +33 -11
  9. package/convex/revalidate.ts +7 -5
  10. package/convex/triggers.ts +53 -3
  11. package/dist/components/ai/ai-chat-panel.js +13 -2
  12. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  13. package/dist/components/app-views/default-modules.d.ts +20 -20
  14. package/dist/components/app-views/default-modules.js +27 -48
  15. package/dist/components/app-views/default-modules.js.map +1 -1
  16. package/dist/components/app-views/studio-config.d.ts +5 -6
  17. package/dist/components/app-views/studio-config.js.map +1 -1
  18. package/dist/components/assets/asset-metadata.d.ts +28 -19
  19. package/dist/components/assets/asset-metadata.js +24 -31
  20. package/dist/components/assets/asset-metadata.js.map +1 -1
  21. package/dist/components/assets/asset-selection.d.ts +24 -5
  22. package/dist/components/assets/asset-selection.js +39 -9
  23. package/dist/components/assets/asset-selection.js.map +1 -1
  24. package/dist/components/assets/assets-collection-view-source.d.ts +17 -14
  25. package/dist/components/assets/assets-collection-view-source.js +25 -31
  26. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  27. package/dist/components/builder/blocks-model.d.ts +5 -4
  28. package/dist/components/builder/blocks-model.js +5 -4
  29. package/dist/components/builder/blocks-model.js.map +1 -1
  30. package/dist/components/builder/blocks-panel.js +3 -3
  31. package/dist/components/builder/blocks-panel.js.map +1 -1
  32. package/dist/components/builder/builder-store.d.ts +25 -1
  33. package/dist/components/builder/builder-store.js +3 -2
  34. package/dist/components/builder/builder-store.js.map +1 -1
  35. package/dist/components/builder/canvas-chrome-css.js +0 -17
  36. package/dist/components/builder/canvas-chrome-css.js.map +1 -1
  37. package/dist/components/builder/canvas-chrome.d.ts +10 -12
  38. package/dist/components/builder/canvas-chrome.js +12 -13
  39. package/dist/components/builder/canvas-chrome.js.map +1 -1
  40. package/dist/components/builder/canvas-dnd.d.ts +8 -0
  41. package/dist/components/builder/canvas-dnd.js +5 -3
  42. package/dist/components/builder/canvas-dnd.js.map +1 -1
  43. package/dist/components/builder/canvas-render-context.d.ts +12 -24
  44. package/dist/components/builder/canvas-render-context.js +15 -44
  45. package/dist/components/builder/canvas-render-context.js.map +1 -1
  46. package/dist/components/builder/canvas.js +5 -11
  47. package/dist/components/builder/canvas.js.map +1 -1
  48. package/dist/components/builder/clipboard-actions.d.ts +15 -1
  49. package/dist/components/builder/clipboard-actions.js +35 -12
  50. package/dist/components/builder/clipboard-actions.js.map +1 -1
  51. package/dist/components/builder/composition-field-editor.d.ts +32 -26
  52. package/dist/components/builder/composition-field-editor.js +47 -54
  53. package/dist/components/builder/composition-field-editor.js.map +1 -1
  54. package/dist/components/builder/insert-context.d.ts +3 -1
  55. package/dist/components/builder/insert-context.js +19 -12
  56. package/dist/components/builder/insert-context.js.map +1 -1
  57. package/dist/components/builder/page-tree.d.ts +29 -0
  58. package/dist/components/builder/page-tree.js +56 -58
  59. package/dist/components/builder/page-tree.js.map +1 -1
  60. package/dist/components/builder/reuse.js +5 -2
  61. package/dist/components/builder/reuse.js.map +1 -1
  62. package/dist/components/cms/asset-field.js +20 -32
  63. package/dist/components/cms/asset-field.js.map +1 -1
  64. package/dist/components/cms/cell-edit.d.ts +7 -0
  65. package/dist/components/cms/cell-edit.js +9 -1
  66. package/dist/components/cms/cell-edit.js.map +1 -1
  67. package/dist/components/cms/cms-composition-validators.js +11 -2
  68. package/dist/components/cms/cms-composition-validators.js.map +1 -1
  69. package/dist/components/cms/collection-table.js +11 -0
  70. package/dist/components/cms/collection-table.js.map +1 -1
  71. package/dist/components/cms/document-url-path.d.ts +7 -4
  72. package/dist/components/cms/document-url-path.js +15 -1
  73. package/dist/components/cms/document-url-path.js.map +1 -1
  74. package/dist/components/cms/editable-cell.js +5 -2
  75. package/dist/components/cms/editable-cell.js.map +1 -1
  76. package/dist/components/cms/field-control.js +4 -0
  77. package/dist/components/cms/field-control.js.map +1 -1
  78. package/dist/components/cms/inline-field-errors.d.ts +36 -0
  79. package/dist/components/cms/inline-field-errors.js +13 -5
  80. package/dist/components/cms/inline-field-errors.js.map +1 -1
  81. package/dist/components/cms/rich-text/insert.d.ts +6 -3
  82. package/dist/components/cms/rich-text/insert.js +9 -6
  83. package/dist/components/cms/rich-text/insert.js.map +1 -1
  84. package/dist/components/cms/use-document-versions.js +1 -0
  85. package/dist/components/cms/use-document-versions.js.map +1 -1
  86. package/dist/components/cms/use-live-url.d.ts +18 -6
  87. package/dist/components/cms/use-live-url.js +10 -0
  88. package/dist/components/cms/use-live-url.js.map +1 -1
  89. package/dist/components/cms/versions-model.d.ts +0 -12
  90. package/dist/components/cms/versions-model.js +3 -15
  91. package/dist/components/cms/versions-model.js.map +1 -1
  92. package/dist/components/components/component-detail.js +1 -1
  93. package/dist/components/components/component-detail.js.map +1 -1
  94. package/dist/components/components/transfer-dialog.js +2 -4
  95. package/dist/components/components/transfer-dialog.js.map +1 -1
  96. package/dist/components/components/transfer.d.ts +19 -15
  97. package/dist/components/components/transfer.js +28 -25
  98. package/dist/components/components/transfer.js.map +1 -1
  99. package/dist/components/deployment-setup-gate.js +9 -3
  100. package/dist/components/deployment-setup-gate.js.map +1 -1
  101. package/dist/components/project-session.d.ts +9 -0
  102. package/dist/components/project-session.js +1 -1
  103. package/dist/components/project-session.js.map +1 -1
  104. package/dist/components/redirects/redirects-collection-view-source.d.ts +17 -2
  105. package/dist/components/redirects/redirects-collection-view-source.js +20 -6
  106. package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
  107. package/dist/components/section-surface.js +3 -2
  108. package/dist/components/section-surface.js.map +1 -1
  109. package/dist/components/settings-form.js +1 -2
  110. package/dist/components/settings-form.js.map +1 -1
  111. package/dist/components/studio-context.js +1 -18
  112. package/dist/components/studio-context.js.map +1 -1
  113. package/dist/components/studio-project-screen.js +3 -5
  114. package/dist/components/studio-project-screen.js.map +1 -1
  115. package/dist/lib/ai/apply-content.js +73 -29
  116. package/dist/lib/ai/apply-content.js.map +1 -1
  117. package/dist/lib/ai/constraints.d.ts +35 -0
  118. package/dist/lib/ai/constraints.js +155 -72
  119. package/dist/lib/ai/constraints.js.map +1 -1
  120. package/dist/lib/ai/index.d.ts +1 -0
  121. package/dist/lib/ai/index.js +1 -0
  122. package/dist/lib/ai/index.js.map +1 -1
  123. package/dist/lib/ai/route-fetch.d.ts +58 -0
  124. package/dist/lib/ai/route-fetch.js +98 -0
  125. package/dist/lib/ai/route-fetch.js.map +1 -0
  126. package/dist/lib/ai/route-provider.d.ts +5 -22
  127. package/dist/lib/ai/route-provider.js +23 -1
  128. package/dist/lib/ai/route-provider.js.map +1 -1
  129. package/dist/lib/ai/route.js +4 -1
  130. package/dist/lib/ai/route.js.map +1 -1
  131. package/dist/lib/ai/shape.d.ts +3 -2
  132. package/dist/lib/ai/shape.js +16 -24
  133. package/dist/lib/ai/shape.js.map +1 -1
  134. package/dist/lib/ai/types.d.ts +8 -0
  135. package/dist/lib/ai/types.js.map +1 -1
  136. package/dist/lib/ai/update-fields.js +33 -12
  137. package/dist/lib/ai/update-fields.js.map +1 -1
  138. package/dist/lib/assets/asset-placements.d.ts +35 -0
  139. package/dist/lib/assets/asset-placements.js +74 -0
  140. package/dist/lib/assets/asset-placements.js.map +1 -0
  141. package/dist/lib/assets/backfill-thumbs.d.ts +9 -8
  142. package/dist/lib/assets/backfill-thumbs.js +13 -25
  143. package/dist/lib/assets/backfill-thumbs.js.map +1 -1
  144. package/dist/lib/clipboard/paste-issues.d.ts +17 -15
  145. package/dist/lib/clipboard/paste-issues.js +8 -45
  146. package/dist/lib/clipboard/paste-issues.js.map +1 -1
  147. package/dist/lib/clipboard/paste-target.d.ts +47 -1
  148. package/dist/lib/clipboard/paste-target.js +19 -11
  149. package/dist/lib/clipboard/paste-target.js.map +1 -1
  150. package/dist/lib/cms/document-gate.d.ts +19 -9
  151. package/dist/lib/cms/document-gate.js +25 -4
  152. package/dist/lib/cms/document-gate.js.map +1 -1
  153. package/dist/lib/cms/published-documents.js +1 -0
  154. package/dist/lib/cms/published-documents.js.map +1 -1
  155. package/dist/lib/cms-views/view-model.d.ts +3 -3
  156. package/dist/lib/data/index.d.ts +0 -3
  157. package/dist/lib/data/index.js +4 -6
  158. package/dist/lib/data/index.js.map +1 -1
  159. package/dist/lib/dnd/drop-rules.d.ts +45 -2
  160. package/dist/lib/dnd/drop-rules.js +64 -9
  161. package/dist/lib/dnd/drop-rules.js.map +1 -1
  162. package/dist/lib/engine-version.d.ts +16 -0
  163. package/dist/lib/engine-version.js +17 -0
  164. package/dist/lib/engine-version.js.map +1 -0
  165. package/dist/lib/i18n/messages.de.js +8 -56
  166. package/dist/lib/i18n/messages.de.js.map +1 -1
  167. package/dist/lib/i18n/messages.en.d.ts +8 -53
  168. package/dist/lib/i18n/messages.en.js +15 -57
  169. package/dist/lib/i18n/messages.en.js.map +1 -1
  170. package/dist/lib/locale/fallback.js +8 -2
  171. package/dist/lib/locale/fallback.js.map +1 -1
  172. package/dist/lib/locale/index.d.ts +0 -1
  173. package/dist/lib/locale/index.js +0 -1
  174. package/dist/lib/locale/index.js.map +1 -1
  175. package/dist/lib/locale/locale-store.d.ts +10 -3
  176. package/dist/lib/locale/locale-store.js +12 -5
  177. package/dist/lib/locale/locale-store.js.map +1 -1
  178. package/dist/lib/operations/session-hydration.d.ts +9 -5
  179. package/dist/lib/operations/session-hydration.js +10 -10
  180. package/dist/lib/operations/session-hydration.js.map +1 -1
  181. package/dist/lib/operations/studio-operations.js +4 -2
  182. package/dist/lib/operations/studio-operations.js.map +1 -1
  183. package/dist/lib/persistence/convex-adapter.js +63 -75
  184. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  185. package/dist/lib/persistence/deployment-readiness.d.ts +8 -1
  186. package/dist/lib/persistence/deployment-readiness.js +4 -0
  187. package/dist/lib/persistence/deployment-readiness.js.map +1 -1
  188. package/dist/lib/persona/labels.d.ts +0 -2
  189. package/dist/lib/persona/labels.js +1 -7
  190. package/dist/lib/persona/labels.js.map +1 -1
  191. package/dist/lib/registry/index.d.ts +4 -2
  192. package/dist/lib/registry/index.js +26 -21
  193. package/dist/lib/registry/index.js.map +1 -1
  194. package/dist/lib/registry/project-components.js +0 -3
  195. package/dist/lib/registry/project-components.js.map +1 -1
  196. package/dist/lib/routes/route-issues.js +4 -5
  197. package/dist/lib/routes/route-issues.js.map +1 -1
  198. package/dist/lib/theme.js +3 -2
  199. package/dist/lib/theme.js.map +1 -1
  200. package/dist/lib/webhooks/tags.js +11 -14
  201. package/dist/lib/webhooks/tags.js.map +1 -1
  202. package/dist/lib/workspace/index.d.ts +1 -1
  203. package/dist/lib/workspace/index.js +1 -1
  204. package/dist/lib/workspace/index.js.map +1 -1
  205. package/dist/previews/enumerate.d.ts +27 -9
  206. package/dist/previews/enumerate.js +27 -6
  207. package/dist/previews/enumerate.js.map +1 -1
  208. package/dist/previews/manifest-format.d.ts +16 -7
  209. package/dist/previews/manifest-format.js +0 -21
  210. package/dist/previews/manifest-format.js.map +1 -1
  211. package/dist/router/match.d.ts +0 -2
  212. package/dist/router/match.js +0 -2
  213. package/dist/router/match.js.map +1 -1
  214. package/dist/ui/badge.d.ts +1 -1
  215. package/package.json +2 -2
  216. package/src/styles/studio.css +5 -4
  217. package/src/styles/tokens.css +72 -55
  218. package/convex/content.ts +0 -291
  219. package/dist/components/collab/collab-context.d.ts +0 -27
  220. package/dist/components/collab/collab-context.js +0 -39
  221. package/dist/components/collab/collab-context.js.map +0 -1
  222. package/dist/components/collab/collab-seed.d.ts +0 -15
  223. package/dist/components/collab/collab-seed.js +0 -20
  224. package/dist/components/collab/collab-seed.js.map +0 -1
  225. package/dist/components/extensions/extensions-view.d.ts +0 -7
  226. package/dist/components/extensions/extensions-view.js +0 -127
  227. package/dist/components/extensions/extensions-view.js.map +0 -1
  228. package/dist/lib/bindings/section-bindings.d.ts +0 -24
  229. package/dist/lib/bindings/section-bindings.js +0 -9
  230. package/dist/lib/bindings/section-bindings.js.map +0 -1
  231. package/dist/lib/data/data-store.d.ts +0 -98
  232. package/dist/lib/data/data-store.js +0 -211
  233. package/dist/lib/data/data-store.js.map +0 -1
  234. package/dist/lib/data/providers.d.ts +0 -143
  235. package/dist/lib/data/providers.js +0 -255
  236. package/dist/lib/data/providers.js.map +0 -1
  237. package/dist/lib/data/sample-capture.d.ts +0 -30
  238. package/dist/lib/data/sample-capture.js +0 -70
  239. package/dist/lib/data/sample-capture.js.map +0 -1
  240. package/dist/lib/data/sources-store.d.ts +0 -42
  241. package/dist/lib/data/sources-store.js +0 -120
  242. package/dist/lib/data/sources-store.js.map +0 -1
  243. package/dist/lib/locale/binding-fallback.d.ts +0 -35
  244. package/dist/lib/locale/binding-fallback.js +0 -73
  245. package/dist/lib/locale/binding-fallback.js.map +0 -1
  246. package/dist/lib/starters/kit-session.d.ts +0 -5
  247. package/dist/lib/starters/kit-session.js +0 -17
  248. package/dist/lib/starters/kit-session.js.map +0 -1
@@ -2,19 +2,24 @@ import type { ComponentNode } from '@elytracms/core/project-graph';
2
2
  /**
3
3
  * Permissive-paste validation (EC-175): pasting NEVER refuses a structurally
4
4
  * valid fragment. Instead, the inserted subtree is diffed against the TARGET
5
- * project's registry and data sources at paste time (cross-project semantics:
6
- * the fragment's source project is irrelevant — what matters is what *this*
7
- * project can resolve), and every mismatch becomes a visible, node-addressed
8
- * issue the notice deep-links. The renderer's existing fallbacks keep the
9
- * pasted nodes visible meanwhile.
5
+ * project's registry at paste time (cross-project semantics: the fragment's
6
+ * source project is irrelevant — what matters is what *this* project can
7
+ * resolve), and every mismatch becomes a visible, node-addressed issue the
8
+ * notice deep-links. The renderer's existing fallbacks keep the pasted nodes
9
+ * visible meanwhile.
10
10
  *
11
- * EC-494: this diff runs STUDIO-side (it needs the target registry and the session's
12
- * data sources), so it holds the active locale and can call `translate` directly —
13
- * no message ref has to cross a boundary, unlike the engine-built clipboard errors
14
- * in `@elytracms/core/clipboard`.
11
+ * EC-574: the second half of this diff — binding source ids the target could
12
+ * not resolve — went with the data-binding subsystem. A pasted node carrying a
13
+ * retired binding member is reported by the composition validator
14
+ * (`legacy-node-member`) when the document is validated, not here.
15
+ *
16
+ * EC-494: this diff runs STUDIO-side (it needs the target registry), so it holds
17
+ * the active locale and can call `translate` directly — no message ref has to
18
+ * cross a boundary, unlike the engine-built clipboard errors in
19
+ * `@elytracms/core/clipboard`.
15
20
  */
16
21
  export interface PasteIssue {
17
- kind: 'missing-component' | 'unresolved-binding';
22
+ kind: 'missing-component';
18
23
  message: string;
19
24
  /** The inserted node carrying the problem — deep-link target. */
20
25
  nodeId: string;
@@ -22,18 +27,15 @@ export interface PasteIssue {
22
27
  export interface PasteDiff {
23
28
  /** Distinct component ids absent from the target registry. */
24
29
  missingComponents: string[];
25
- /** Distinct binding source ids the target cannot resolve. */
26
- unresolvedBindings: string[];
27
30
  issues: PasteIssue[];
28
31
  }
29
32
  /**
30
33
  * Diff an inserted subtree (with its final, session-assigned node ids)
31
- * against the target project's registry + known data sources.
34
+ * against the target project's registry.
32
35
  */
33
36
  export declare function diffPastedSubtree(args: {
34
37
  node: ComponentNode;
35
38
  hasComponent: (componentId: string) => boolean;
36
- knownSourceIds: ReadonlySet<string>;
37
39
  }): PasteDiff;
38
- /** “Pasted — 2 bindings unresolved, 1 component missing”, or just “Pasted”. */
40
+ /** “Pasted — 1 component missing”, or just “Pasted”. */
39
41
  export declare function pasteSummaryMessage(diff: PasteDiff): string;
@@ -8,62 +8,25 @@ function walk(node, visit) {
8
8
  }
9
9
  /**
10
10
  * Diff an inserted subtree (with its final, session-assigned node ids)
11
- * against the target project's registry + known data sources.
11
+ * against the target project's registry.
12
12
  */
13
13
  export function diffPastedSubtree(args) {
14
14
  const missingComponents = new Map(); // componentId -> first nodeId
15
- const unresolvedBindings = new Map(); // nodeId+sourceId -> issue
16
- const unresolvedSourceIds = new Set();
17
15
  walk(args.node, (node) => {
18
16
  if (!args.hasComponent(node.componentId) && !missingComponents.has(node.componentId)) {
19
17
  missingComponents.set(node.componentId, node.id);
20
18
  }
21
- const bindings = [];
22
- for (const value of Object.values(node.props ?? {})) {
23
- if (value.kind === 'binding')
24
- bindings.push(value.binding);
25
- }
26
- if (node.condition)
27
- bindings.push(node.condition.source);
28
- for (const binding of bindings) {
29
- if (args.knownSourceIds.has(binding.sourceId))
30
- continue;
31
- unresolvedSourceIds.add(binding.sourceId);
32
- const key = `${node.id} ${binding.sourceId}`;
33
- if (!unresolvedBindings.has(key)) {
34
- unresolvedBindings.set(key, {
35
- kind: 'unresolved-binding',
36
- message: translate('builder.clipboard.issue.unresolvedBinding', {
37
- sourceId: binding.sourceId,
38
- }),
39
- nodeId: node.id,
40
- });
41
- }
42
- }
43
19
  });
44
- const issues = [
45
- ...[...unresolvedBindings.values()],
46
- ...[...missingComponents.entries()].map(([componentId, nodeId]) => ({
47
- kind: 'missing-component',
48
- message: translate('builder.clipboard.issue.missingComponent', { componentId }),
49
- nodeId,
50
- })),
51
- ];
52
- return {
53
- missingComponents: [...missingComponents.keys()],
54
- unresolvedBindings: [...unresolvedSourceIds],
55
- issues,
56
- };
20
+ const issues = [...missingComponents.entries()].map(([componentId, nodeId]) => ({
21
+ kind: 'missing-component',
22
+ message: translate('builder.clipboard.issue.missingComponent', { componentId }),
23
+ nodeId,
24
+ }));
25
+ return { missingComponents: [...missingComponents.keys()], issues };
57
26
  }
58
- /** “Pasted — 2 bindings unresolved, 1 component missing”, or just “Pasted”. */
27
+ /** “Pasted — 1 component missing”, or just “Pasted”. */
59
28
  export function pasteSummaryMessage(diff) {
60
29
  const parts = [];
61
- const bindings = diff.issues.filter((i) => i.kind === 'unresolved-binding').length;
62
- if (bindings > 0) {
63
- parts.push(translate(bindings === 1
64
- ? 'builder.clipboard.summary.bindingUnresolvedOne'
65
- : 'builder.clipboard.summary.bindingsUnresolved', { count: bindings }));
66
- }
67
30
  if (diff.missingComponents.length > 0) {
68
31
  const n = diff.missingComponents.length;
69
32
  parts.push(translate(n === 1
@@ -1 +1 @@
1
- {"version":3,"file":"paste-issues.js","sourceRoot":"","sources":["../../../src/lib/clipboard/paste-issues.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BindingReference, ComponentNode } from '@elytracms/core/project-graph'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * Permissive-paste validation (EC-175): pasting NEVER refuses a structurally\n * valid fragment. Instead, the inserted subtree is diffed against the TARGET\n * project's registry and data sources at paste time (cross-project semantics:\n * the fragment's source project is irrelevant — what matters is what *this*\n * project can resolve), and every mismatch becomes a visible, node-addressed\n * issue the notice deep-links. The renderer's existing fallbacks keep the\n * pasted nodes visible meanwhile.\n *\n * EC-494: this diff runs STUDIO-side (it needs the target registry and the session's\n * data sources), so it holds the active locale and can call `translate` directly —\n * no message ref has to cross a boundary, unlike the engine-built clipboard errors\n * in `@elytracms/core/clipboard`.\n */\n\nexport interface PasteIssue {\n kind: 'missing-component' | 'unresolved-binding'\n message: string\n /** The inserted node carrying the problem — deep-link target. */\n nodeId: string\n}\n\nexport interface PasteDiff {\n /** Distinct component ids absent from the target registry. */\n missingComponents: string[]\n /** Distinct binding source ids the target cannot resolve. */\n unresolvedBindings: string[]\n issues: PasteIssue[]\n}\n\nfunction walk(node: ComponentNode, visit: (node: ComponentNode) => void): void {\n visit(node)\n for (const children of Object.values(node.slots ?? {})) {\n for (const child of children) walk(child, visit)\n }\n}\n\n/**\n * Diff an inserted subtree (with its final, session-assigned node ids)\n * against the target project's registry + known data sources.\n */\nexport function diffPastedSubtree(args: {\n node: ComponentNode\n hasComponent: (componentId: string) => boolean\n knownSourceIds: ReadonlySet<string>\n}): PasteDiff {\n const missingComponents = new Map<string, string>() // componentId -> first nodeId\n const unresolvedBindings = new Map<string, PasteIssue>() // nodeId+sourceId -> issue\n const unresolvedSourceIds = new Set<string>()\n walk(args.node, (node) => {\n if (!args.hasComponent(node.componentId) && !missingComponents.has(node.componentId)) {\n missingComponents.set(node.componentId, node.id)\n }\n const bindings: BindingReference[] = []\n for (const value of Object.values(node.props ?? {})) {\n if (value.kind === 'binding') bindings.push(value.binding)\n }\n if (node.condition) bindings.push(node.condition.source)\n for (const binding of bindings) {\n if (args.knownSourceIds.has(binding.sourceId)) continue\n unresolvedSourceIds.add(binding.sourceId)\n const key = `${node.id} ${binding.sourceId}`\n if (!unresolvedBindings.has(key)) {\n unresolvedBindings.set(key, {\n kind: 'unresolved-binding',\n message: translate('builder.clipboard.issue.unresolvedBinding', {\n sourceId: binding.sourceId,\n }),\n nodeId: node.id,\n })\n }\n }\n })\n const issues: PasteIssue[] = [\n ...[...unresolvedBindings.values()],\n ...[...missingComponents.entries()].map(\n ([componentId, nodeId]): PasteIssue => ({\n kind: 'missing-component',\n message: translate('builder.clipboard.issue.missingComponent', { componentId }),\n nodeId,\n }),\n ),\n ]\n return {\n missingComponents: [...missingComponents.keys()],\n unresolvedBindings: [...unresolvedSourceIds],\n issues,\n }\n}\n\n/** “Pasted — 2 bindings unresolved, 1 component missing”, or just “Pasted”. */\nexport function pasteSummaryMessage(diff: PasteDiff): string {\n const parts: string[] = []\n const bindings = diff.issues.filter((i) => i.kind === 'unresolved-binding').length\n if (bindings > 0) {\n parts.push(\n translate(\n bindings === 1\n ? 'builder.clipboard.summary.bindingUnresolvedOne'\n : 'builder.clipboard.summary.bindingsUnresolved',\n { count: bindings },\n ),\n )\n }\n if (diff.missingComponents.length > 0) {\n const n = diff.missingComponents.length\n parts.push(\n translate(\n n === 1\n ? 'builder.clipboard.summary.componentMissingOne'\n : 'builder.clipboard.summary.componentsMissing',\n { count: n },\n ),\n )\n }\n return parts.length > 0\n ? translate('builder.clipboard.summary.pastedWith', { parts: parts.join(', ') })\n : translate('builder.clipboard.summary.pasted')\n}\n"]}
1
+ {"version":3,"file":"paste-issues.js","sourceRoot":"","sources":["../../../src/lib/clipboard/paste-issues.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAmCtD,SAAS,IAAI,CAAC,IAAmB,EAAE,KAAoC;IACrE,KAAK,CAAC,IAAI,CAAC,CAAA;IACX,KAAK,MAAM,QAAQ,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,CAAC;QACvD,KAAK,MAAM,KAAK,IAAI,QAAQ;YAAE,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;IAClD,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAGjC;IACC,MAAM,iBAAiB,GAAG,IAAI,GAAG,EAAkB,CAAA,CAAC,8BAA8B;IAClF,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE;QACvB,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;YACrF,iBAAiB,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,EAAE,CAAC,CAAA;QAClD,CAAC;IACH,CAAC,CAAC,CAAA;IACF,MAAM,MAAM,GAAiB,CAAC,GAAG,iBAAiB,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAC/D,CAAC,CAAC,WAAW,EAAE,MAAM,CAAC,EAAc,EAAE,CAAC,CAAC;QACtC,IAAI,EAAE,mBAAmB;QACzB,OAAO,EAAE,SAAS,CAAC,0CAA0C,EAAE,EAAE,WAAW,EAAE,CAAC;QAC/E,MAAM;KACP,CAAC,CACH,CAAA;IACD,OAAO,EAAE,iBAAiB,EAAE,CAAC,GAAG,iBAAiB,CAAC,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,CAAA;AACrE,CAAC;AAED,wDAAwD;AACxD,MAAM,UAAU,mBAAmB,CAAC,IAAe;IACjD,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,IAAI,IAAI,CAAC,iBAAiB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtC,MAAM,CAAC,GAAG,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAA;QACvC,KAAK,CAAC,IAAI,CACR,SAAS,CACP,CAAC,KAAK,CAAC;YACL,CAAC,CAAC,+CAA+C;YACjD,CAAC,CAAC,6CAA6C,EACjD,EAAE,KAAK,EAAE,CAAC,EAAE,CACb,CACF,CAAA;IACH,CAAC;IACD,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC;QACrB,CAAC,CAAC,SAAS,CAAC,sCAAsC,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAChF,CAAC,CAAC,SAAS,CAAC,kCAAkC,CAAC,CAAA;AACnD,CAAC","sourcesContent":["import type { ComponentNode } from '@elytracms/core/project-graph'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * Permissive-paste validation (EC-175): pasting NEVER refuses a structurally\n * valid fragment. Instead, the inserted subtree is diffed against the TARGET\n * project's registry at paste time (cross-project semantics: the fragment's\n * source project is irrelevant — what matters is what *this* project can\n * resolve), and every mismatch becomes a visible, node-addressed issue the\n * notice deep-links. The renderer's existing fallbacks keep the pasted nodes\n * visible meanwhile.\n *\n * EC-574: the second half of this diff — binding source ids the target could\n * not resolve — went with the data-binding subsystem. A pasted node carrying a\n * retired binding member is reported by the composition validator\n * (`legacy-node-member`) when the document is validated, not here.\n *\n * EC-494: this diff runs STUDIO-side (it needs the target registry), so it holds\n * the active locale and can call `translate` directly — no message ref has to\n * cross a boundary, unlike the engine-built clipboard errors in\n * `@elytracms/core/clipboard`.\n */\n\nexport interface PasteIssue {\n kind: 'missing-component'\n message: string\n /** The inserted node carrying the problem — deep-link target. */\n nodeId: string\n}\n\nexport interface PasteDiff {\n /** Distinct component ids absent from the target registry. */\n missingComponents: string[]\n issues: PasteIssue[]\n}\n\nfunction walk(node: ComponentNode, visit: (node: ComponentNode) => void): void {\n visit(node)\n for (const children of Object.values(node.slots ?? {})) {\n for (const child of children) walk(child, visit)\n }\n}\n\n/**\n * Diff an inserted subtree (with its final, session-assigned node ids)\n * against the target project's registry.\n */\nexport function diffPastedSubtree(args: {\n node: ComponentNode\n hasComponent: (componentId: string) => boolean\n}): PasteDiff {\n const missingComponents = new Map<string, string>() // componentId -> first nodeId\n walk(args.node, (node) => {\n if (!args.hasComponent(node.componentId) && !missingComponents.has(node.componentId)) {\n missingComponents.set(node.componentId, node.id)\n }\n })\n const issues: PasteIssue[] = [...missingComponents.entries()].map(\n ([componentId, nodeId]): PasteIssue => ({\n kind: 'missing-component',\n message: translate('builder.clipboard.issue.missingComponent', { componentId }),\n nodeId,\n }),\n )\n return { missingComponents: [...missingComponents.keys()], issues }\n}\n\n/** “Pasted — 1 component missing”, or just “Pasted”. */\nexport function pasteSummaryMessage(diff: PasteDiff): string {\n const parts: string[] = []\n if (diff.missingComponents.length > 0) {\n const n = diff.missingComponents.length\n parts.push(\n translate(\n n === 1\n ? 'builder.clipboard.summary.componentMissingOne'\n : 'builder.clipboard.summary.componentsMissing',\n { count: n },\n ),\n )\n }\n return parts.length > 0\n ? translate('builder.clipboard.summary.pastedWith', { parts: parts.join(', ') })\n : translate('builder.clipboard.summary.pasted')\n}\n"]}
@@ -6,15 +6,61 @@ import type { ComponentManifest } from '@elytracms/core/component-registry';
6
6
  * the anchor node's child slot when it has one, else *after* the anchor as a
7
7
  * sibling, else appended to the page root. Pure and selection-driven so the
8
8
  * canvas, the page tree, and keyboard paste all share one rule.
9
+ *
10
+ * EC-545: the resolved slot's `allow` leash is checked HERE, for the pasted
11
+ * subtree's root, through the same `slotAllows` the drop rule reads — so a
12
+ * component drag-and-drop refuses is refused by clipboard too, and a caller
13
+ * cannot reach an `InsertTarget` without passing the check (the rejection is a
14
+ * separate member of the result union, not a flag beside the target). Nested
15
+ * validity of the pasted subtree stays with the publish gate, exactly like a
16
+ * drop.
17
+ *
18
+ * EC-554: the composition FIELD's top-level `allow` is checked here too, through
19
+ * the same `placementRefusal` → `fieldAllowAt` the drop rule and the insert offer
20
+ * read — a paste that lands as a direct child of the composition root must be in
21
+ * the field's vocabulary.
9
22
  */
10
23
  export interface PasteTarget {
24
+ ok: true;
11
25
  target: InsertTarget;
12
26
  /** Human label of where the paste lands (notices/debugging). */
13
27
  destinationLabel: string;
14
28
  }
29
+ /** The resolved slot's own `allow` list excludes the pasted root (EC-545). */
30
+ export interface PasteSlotRejection {
31
+ ok: false;
32
+ code: 'not-allowed';
33
+ /** The pasted root the slot refuses. */
34
+ componentId: string;
35
+ /** The resolved slot whose `allow` list excludes it. */
36
+ slot: string;
37
+ /** Human label of the refused destination (`Section · body`). */
38
+ destinationLabel: string;
39
+ }
40
+ /**
41
+ * The composition field's top-level `allow` excludes the pasted root (EC-554).
42
+ * Deliberately carries NO slot/destination label: the refused slot is the
43
+ * invisible synthetic root's (`Stack · children`), and the reason to show is the
44
+ * FIELD — the notice names it from the editor's field scope.
45
+ */
46
+ export interface PasteFieldRejection {
47
+ ok: false;
48
+ code: 'field-not-allowed';
49
+ /** The pasted root the field vocabulary refuses. */
50
+ componentId: string;
51
+ }
52
+ export type PasteRejection = PasteSlotRejection | PasteFieldRejection;
53
+ export type PasteResolution = PasteTarget | PasteRejection;
15
54
  export declare function resolvePasteTarget(args: {
16
55
  root: ComponentNode;
17
56
  /** The anchor node (usually the selection); `null` falls back to the root. */
18
57
  anchorId: string | null;
58
+ /** The pasted subtree's root component — the id the slot leash judges. */
59
+ componentId: string;
19
60
  getManifest: (componentId: string) => ComponentManifest | undefined;
20
- }): PasteTarget;
61
+ /**
62
+ * The composition field's top-level `allow` (EC-554) — `undefined` when the
63
+ * field declares none. Required so no caller can forget to pass it.
64
+ */
65
+ fieldAllow: readonly string[] | undefined;
66
+ }): PasteResolution;
@@ -1,29 +1,37 @@
1
1
  import { getNode, getParentInfo } from '@elytracms/core/project-graph';
2
- import { manifestRules, primaryInsertSlot } from '@elytracms/studio/lib/dnd/drop-rules';
2
+ import { manifestRules, placementRefusal, primaryInsertSlot } from '@elytracms/studio/lib/dnd/drop-rules';
3
3
  import { shortId } from '@elytracms/studio/lib/persona/labels';
4
4
  export function resolvePasteTarget(args) {
5
- const { root, getManifest } = args;
5
+ const { root, componentId, getManifest, fieldAllow } = args;
6
6
  const anchor = args.anchorId !== null ? getNode(root, args.anchorId) : undefined;
7
7
  const node = anchor ?? root;
8
+ const leashed = (slot, slotRule, target, destinationLabel) => {
9
+ const refusal = placementRefusal({ root, parentId: target.parentId, slot: slotRule, fieldAllow, componentId });
10
+ if (refusal === 'field')
11
+ return { ok: false, code: 'field-not-allowed', componentId };
12
+ if (refusal === 'slot')
13
+ return { ok: false, code: 'not-allowed', componentId, slot, destinationLabel };
14
+ return { ok: true, target, destinationLabel };
15
+ };
8
16
  // EC-386: the shared slot rule (`primaryInsertSlot` over the merged
9
17
  // `manifestRules` vocabulary) — a blocks-prop region (e.g. a Section's `body`)
10
18
  // receives the paste exactly like the insert dialog and a DnD `into`.
11
19
  const manifest = getManifest(node.componentId);
12
20
  const childSlot = primaryInsertSlot(manifestRules(manifest));
13
21
  if (childSlot) {
14
- return {
15
- target: { parentId: node.id, slot: childSlot.name },
16
- destinationLabel: `${shortId(node.componentId)} · ${childSlot.name}`,
17
- };
22
+ return leashed(childSlot.name, childSlot, { parentId: node.id, slot: childSlot.name }, `${shortId(node.componentId)} · ${childSlot.name}`);
18
23
  }
19
24
  const info = getParentInfo(root, node.id);
20
25
  if (info) {
21
- return {
22
- target: { parentId: info.parent.id, slot: info.slot, index: info.index + 1 },
23
- destinationLabel: `${shortId(info.parent.componentId)} · ${info.slot}`,
24
- };
26
+ // A sibling paste is governed by the PARENT's slot leash (the drop rule's
27
+ // before/after case). An unknown parent manifest imposes no allow-list;
28
+ // validation owns that issue.
29
+ const parentSlot = manifestRules(getManifest(info.parent.componentId))?.slots.find((s) => s.name === info.slot);
30
+ return leashed(info.slot, parentSlot, { parentId: info.parent.id, slot: info.slot, index: info.index + 1 }, `${shortId(info.parent.componentId)} · ${info.slot}`);
25
31
  }
26
32
  // Leaf root with no declared slots (degenerate graphs): append to `children`.
27
- return { target: { parentId: root.id, slot: 'children' }, destinationLabel: 'root' };
33
+ // No slot rule exists, but the paste still lands at the root — the field leash
34
+ // applies.
35
+ return leashed('children', undefined, { parentId: root.id, slot: 'children' }, 'root');
28
36
  }
29
37
  //# sourceMappingURL=paste-target.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"paste-target.js","sourceRoot":"","sources":["../../../src/lib/clipboard/paste-target.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAGtE,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAA;AACvF,OAAO,EAAE,OAAO,EAAE,MAAM,sCAAsC,CAAA;AAgB9D,MAAM,UAAU,kBAAkB,CAAC,IAKlC;IACC,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,IAAI,CAAA;IAClC,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAChF,MAAM,IAAI,GAAG,MAAM,IAAI,IAAI,CAAA;IAE3B,oEAAoE;IACpE,+EAA+E;IAC/E,sEAAsE;IACtE,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;IAC9C,MAAM,SAAS,GAAG,iBAAiB,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,CAAA;IAC5D,IAAI,SAAS,EAAE,CAAC;QACd,OAAO;YACL,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE;YACnD,gBAAgB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,SAAS,CAAC,IAAI,EAAE;SACrE,CAAA;IACH,CAAC;IAED,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,CAAA;IACzC,IAAI,IAAI,EAAE,CAAC;QACT,OAAO;YACL,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC,EAAE;YAC5E,gBAAgB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE;SACvE,CAAA;IACH,CAAC;IAED,8EAA8E;IAC9E,OAAO,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,gBAAgB,EAAE,MAAM,EAAE,CAAA;AACtF,CAAC","sourcesContent":["import { getNode, getParentInfo } from '@elytracms/core/project-graph'\nimport type { ComponentNode, InsertTarget } from '@elytracms/core/project-graph'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { manifestRules, primaryInsertSlot } from '@elytracms/studio/lib/dnd/drop-rules'\nimport { shortId } from '@elytracms/studio/lib/persona/labels'\n\n/**\n * Paste landing-spot resolution (EC-175) — the same placement rule the Insert\n * dialog uses (`insert-context.ts`): a pasted subtree lands *into*\n * the anchor node's child slot when it has one, else *after* the anchor as a\n * sibling, else appended to the page root. Pure and selection-driven so the\n * canvas, the page tree, and keyboard paste all share one rule.\n */\n\nexport interface PasteTarget {\n target: InsertTarget\n /** Human label of where the paste lands (notices/debugging). */\n destinationLabel: string\n}\n\nexport function resolvePasteTarget(args: {\n root: ComponentNode\n /** The anchor node (usually the selection); `null` falls back to the root. */\n anchorId: string | null\n getManifest: (componentId: string) => ComponentManifest | undefined\n}): PasteTarget {\n const { root, getManifest } = args\n const anchor = args.anchorId !== null ? getNode(root, args.anchorId) : undefined\n const node = anchor ?? root\n\n // EC-386: the shared slot rule (`primaryInsertSlot` over the merged\n // `manifestRules` vocabulary) — a blocks-prop region (e.g. a Section's `body`)\n // receives the paste exactly like the insert dialog and a DnD `into`.\n const manifest = getManifest(node.componentId)\n const childSlot = primaryInsertSlot(manifestRules(manifest))\n if (childSlot) {\n return {\n target: { parentId: node.id, slot: childSlot.name },\n destinationLabel: `${shortId(node.componentId)} · ${childSlot.name}`,\n }\n }\n\n const info = getParentInfo(root, node.id)\n if (info) {\n return {\n target: { parentId: info.parent.id, slot: info.slot, index: info.index + 1 },\n destinationLabel: `${shortId(info.parent.componentId)} · ${info.slot}`,\n }\n }\n\n // Leaf root with no declared slots (degenerate graphs): append to `children`.\n return { target: { parentId: root.id, slot: 'children' }, destinationLabel: 'root' }\n}\n"]}
1
+ {"version":3,"file":"paste-target.js","sourceRoot":"","sources":["../../../src/lib/clipboard/paste-target.ts"],"names":[],"mappings":"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","sourcesContent":["import { getNode, getParentInfo } from '@elytracms/core/project-graph'\nimport type { ComponentNode, InsertTarget } from '@elytracms/core/project-graph'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { manifestRules, placementRefusal, primaryInsertSlot } from '@elytracms/studio/lib/dnd/drop-rules'\nimport type { SlotRule } from '@elytracms/studio/lib/dnd/drop-rules'\nimport { shortId } from '@elytracms/studio/lib/persona/labels'\n\n/**\n * Paste landing-spot resolution (EC-175) — the same placement rule the Insert\n * dialog uses (`insert-context.ts`): a pasted subtree lands *into*\n * the anchor node's child slot when it has one, else *after* the anchor as a\n * sibling, else appended to the page root. Pure and selection-driven so the\n * canvas, the page tree, and keyboard paste all share one rule.\n *\n * EC-545: the resolved slot's `allow` leash is checked HERE, for the pasted\n * subtree's root, through the same `slotAllows` the drop rule reads — so a\n * component drag-and-drop refuses is refused by clipboard too, and a caller\n * cannot reach an `InsertTarget` without passing the check (the rejection is a\n * separate member of the result union, not a flag beside the target). Nested\n * validity of the pasted subtree stays with the publish gate, exactly like a\n * drop.\n *\n * EC-554: the composition FIELD's top-level `allow` is checked here too, through\n * the same `placementRefusal` → `fieldAllowAt` the drop rule and the insert offer\n * read — a paste that lands as a direct child of the composition root must be in\n * the field's vocabulary.\n */\n\nexport interface PasteTarget {\n ok: true\n target: InsertTarget\n /** Human label of where the paste lands (notices/debugging). */\n destinationLabel: string\n}\n\n/** The resolved slot's own `allow` list excludes the pasted root (EC-545). */\nexport interface PasteSlotRejection {\n ok: false\n code: 'not-allowed'\n /** The pasted root the slot refuses. */\n componentId: string\n /** The resolved slot whose `allow` list excludes it. */\n slot: string\n /** Human label of the refused destination (`Section · body`). */\n destinationLabel: string\n}\n\n/**\n * The composition field's top-level `allow` excludes the pasted root (EC-554).\n * Deliberately carries NO slot/destination label: the refused slot is the\n * invisible synthetic root's (`Stack · children`), and the reason to show is the\n * FIELD — the notice names it from the editor's field scope.\n */\nexport interface PasteFieldRejection {\n ok: false\n code: 'field-not-allowed'\n /** The pasted root the field vocabulary refuses. */\n componentId: string\n}\n\nexport type PasteRejection = PasteSlotRejection | PasteFieldRejection\n\nexport type PasteResolution = PasteTarget | PasteRejection\n\nexport function resolvePasteTarget(args: {\n root: ComponentNode\n /** The anchor node (usually the selection); `null` falls back to the root. */\n anchorId: string | null\n /** The pasted subtree's root component — the id the slot leash judges. */\n componentId: string\n getManifest: (componentId: string) => ComponentManifest | undefined\n /**\n * The composition field's top-level `allow` (EC-554) — `undefined` when the\n * field declares none. Required so no caller can forget to pass it.\n */\n fieldAllow: readonly string[] | undefined\n}): PasteResolution {\n const { root, componentId, getManifest, fieldAllow } = args\n const anchor = args.anchorId !== null ? getNode(root, args.anchorId) : undefined\n const node = anchor ?? root\n\n const leashed = (\n slot: string,\n slotRule: SlotRule | undefined,\n target: InsertTarget,\n destinationLabel: string,\n ): PasteResolution => {\n const refusal = placementRefusal({ root, parentId: target.parentId, slot: slotRule, fieldAllow, componentId })\n if (refusal === 'field') return { ok: false, code: 'field-not-allowed', componentId }\n if (refusal === 'slot') return { ok: false, code: 'not-allowed', componentId, slot, destinationLabel }\n return { ok: true, target, destinationLabel }\n }\n\n // EC-386: the shared slot rule (`primaryInsertSlot` over the merged\n // `manifestRules` vocabulary) — a blocks-prop region (e.g. a Section's `body`)\n // receives the paste exactly like the insert dialog and a DnD `into`.\n const manifest = getManifest(node.componentId)\n const childSlot = primaryInsertSlot(manifestRules(manifest))\n if (childSlot) {\n return leashed(\n childSlot.name,\n childSlot,\n { parentId: node.id, slot: childSlot.name },\n `${shortId(node.componentId)} · ${childSlot.name}`,\n )\n }\n\n const info = getParentInfo(root, node.id)\n if (info) {\n // A sibling paste is governed by the PARENT's slot leash (the drop rule's\n // before/after case). An unknown parent manifest imposes no allow-list;\n // validation owns that issue.\n const parentSlot = manifestRules(getManifest(info.parent.componentId))?.slots.find((s) => s.name === info.slot)\n return leashed(\n info.slot,\n parentSlot,\n { parentId: info.parent.id, slot: info.slot, index: info.index + 1 },\n `${shortId(info.parent.componentId)} · ${info.slot}`,\n )\n }\n\n // Leaf root with no declared slots (degenerate graphs): append to `children`.\n // No slot rule exists, but the paste still lands at the root — the field leash\n // applies.\n return leashed('children', undefined, { parentId: root.id, slot: 'children' }, 'root')\n}\n"]}
@@ -10,17 +10,26 @@ import type { CmsDocument, CmsValidationIssue } from '@elytracms/core/cms-core';
10
10
  * `createStudioOperations` hands this module's reader to the operations layer
11
11
  * once, at bootstrap.
12
12
  *
13
- * The reader deliberately returns `undefined` when nothing is registered rather
14
- * than an empty issue list: `undefined` means "no session context here" and puts
15
- * the operations gate on its documented adapter-record fallback, whereas `[]`
16
- * would silently claim the document is clean. A gate that cannot see must say so,
17
- * not pass.
13
+ * **EC-546 — a gate that cannot see says so.** The first cut had the reader return
14
+ * `undefined` with nothing registered, meaning "no session context here", and
15
+ * the operations gate answered that by falling back to an optionless
16
+ * `validateDocument` over the adapter's schema record: scalar rules only, no
17
+ * store, no locale config, no composition or embed leash. Measured: a document
18
+ * carrying a component outside its slot's `allow` published through
19
+ * `createStudioOperations` whenever the stack was empty. That is the exact
20
+ * weaker, mode-dependent gate EC-497 removed, kept alive by a mount-state side
21
+ * channel. So the reader now answers with issues ONLY: with nothing registered it
22
+ * returns one blocking `error` issue — the publish is refused and the refusal is
23
+ * visible — and the operations layer no longer has a fallback to take (its
24
+ * `DocumentValidator` cannot return `undefined`).
18
25
  *
19
26
  * This module imports nothing but types, so hanging the seam off it never drags
20
27
  * the CMS session (or the component registry behind the composition validators)
21
28
  * into the operations bootstrap's module graph.
22
29
  */
23
30
  export type SessionDocumentValidator = (document: CmsDocument) => readonly CmsValidationIssue[];
31
+ /** The catalog key of the refusal the reader emits with no session mounted (EC-546). */
32
+ export declare const NO_VALIDATION_SESSION_MESSAGE_KEY = "cms.issueMessage.noValidationSession";
24
33
  /** Publish the session's canonical document validator (`null` clears the stack). */
25
34
  export declare function setSessionDocumentValidator(next: SessionDocumentValidator | null): void;
26
35
  /**
@@ -30,8 +39,9 @@ export declare function setSessionDocumentValidator(next: SessionDocumentValidat
30
39
  */
31
40
  export declare function clearSessionDocumentValidator(owned: SessionDocumentValidator): void;
32
41
  /**
33
- * The reader handed to `createOperations`. `undefined` = no session ⇒ the gate
34
- * falls back; see {@link SessionDocumentValidator} and the operations layer's
35
- * `DocumentValidator`.
42
+ * The reader handed to `createOperations`. The innermost session's verdict, or —
43
+ * with no session mounted — the blocking refusal described on
44
+ * {@link SessionDocumentValidator}. Never `undefined`: the operations layer's
45
+ * `DocumentValidator` has no fallback to offer and no longer accepts one (EC-546).
36
46
  */
37
- export declare function sessionValidateDocument(document: CmsDocument): readonly CmsValidationIssue[] | undefined;
47
+ export declare function sessionValidateDocument(document: CmsDocument): readonly CmsValidationIssue[];
@@ -8,6 +8,25 @@
8
8
  * published. Popping restores the outer session instead.
9
9
  */
10
10
  const registered = [];
11
+ /** The catalog key of the refusal the reader emits with no session mounted (EC-546). */
12
+ export const NO_VALIDATION_SESSION_MESSAGE_KEY = 'cms.issueMessage.noValidationSession';
13
+ /**
14
+ * The verdict with no session mounted: one blocking issue, never a pass and never
15
+ * `undefined`. `custom-validation` is the code for a rule the engine's field
16
+ * vocabulary does not name — this is a host rule ("publish only through a mounted
17
+ * session"), not a finding about the document — and the message carries the
18
+ * meaning. Published via the same `messageRef` the engine's issues use, so the
19
+ * studio's operation-error banner renders it in the active locale (EC-494).
20
+ */
21
+ const NO_SESSION_REFUSAL = [
22
+ {
23
+ code: 'custom-validation',
24
+ severity: 'error',
25
+ message: 'No validation session is mounted — refusing to publish unvalidated.',
26
+ messageRef: { key: NO_VALIDATION_SESSION_MESSAGE_KEY },
27
+ path: [],
28
+ },
29
+ ];
11
30
  /** Publish the session's canonical document validator (`null` clears the stack). */
12
31
  export function setSessionDocumentValidator(next) {
13
32
  if (next === null) {
@@ -30,11 +49,13 @@ export function clearSessionDocumentValidator(owned) {
30
49
  registered.splice(at, 1);
31
50
  }
32
51
  /**
33
- * The reader handed to `createOperations`. `undefined` = no session ⇒ the gate
34
- * falls back; see {@link SessionDocumentValidator} and the operations layer's
35
- * `DocumentValidator`.
52
+ * The reader handed to `createOperations`. The innermost session's verdict, or —
53
+ * with no session mounted — the blocking refusal described on
54
+ * {@link SessionDocumentValidator}. Never `undefined`: the operations layer's
55
+ * `DocumentValidator` has no fallback to offer and no longer accepts one (EC-546).
36
56
  */
37
57
  export function sessionValidateDocument(document) {
38
- return registered.at(-1)?.(document);
58
+ const session = registered.at(-1);
59
+ return session ? session(document) : NO_SESSION_REFUSAL;
39
60
  }
40
61
  //# sourceMappingURL=document-gate.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"document-gate.js","sourceRoot":"","sources":["../../../src/lib/cms/document-gate.ts"],"names":[],"mappings":"AAyBA;;;;;;;;GAQG;AACH,MAAM,UAAU,GAA+B,EAAE,CAAA;AAEjD,oFAAoF;AACpF,MAAM,UAAU,2BAA2B,CAAC,IAAqC;IAC/E,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAClB,UAAU,CAAC,MAAM,GAAG,CAAC,CAAA;QACrB,OAAM;IACR,CAAC;IACD,MAAM,EAAE,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IACnC,IAAI,EAAE,IAAI,CAAC;QAAE,UAAU,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;IACrC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACvB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,6BAA6B,CAAC,KAA+B;IAC3E,MAAM,EAAE,GAAG,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;IACpC,IAAI,EAAE,IAAI,CAAC;QAAE,UAAU,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;AACvC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CACrC,QAAqB;IAErB,OAAO,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAA;AACtC,CAAC","sourcesContent":["import type { CmsDocument, CmsValidationIssue } from '@elytracms/core/cms-core'\n\n/**\n * The publish-gate seam (EC-497) — a framework-free module store, the same shape\n * as the current-document seam (`lib/ai-current-document`).\n *\n * `createOperations` is a module singleton built long before (and independently\n * of) any React tree, while gate-strength validation needs the live CMS session:\n * its document store, its asset oracle, its published-ness pointer. So the CMS\n * session PUBLISHES a validator here while mounted, and\n * `createStudioOperations` hands this module's reader to the operations layer\n * once, at bootstrap.\n *\n * The reader deliberately returns `undefined` when nothing is registered rather\n * than an empty issue list: `undefined` means \"no session context here\" and puts\n * the operations gate on its documented adapter-record fallback, whereas `[]`\n * would silently claim the document is clean. A gate that cannot see must say so,\n * not pass.\n *\n * This module imports nothing but types, so hanging the seam off it never drags\n * the CMS session (or the component registry behind the composition validators)\n * into the operations bootstrap's module graph.\n */\nexport type SessionDocumentValidator = (document: CmsDocument) => readonly CmsValidationIssue[]\n\n/**\n * Registered sessions, innermost LAST — a stack, not a single slot, because more\n * than one `CmsProvider` can be mounted at a time: the \"Use in document…\"\n * transfer dialog mounts its own over the app's\n * (`components/transfer-dialog.tsx:92`). With a single slot, closing that dialog\n * would clear the seam and silently drop the publish gate back to its fallback\n * for the rest of the app's life — a regression invisible until something\n * published. Popping restores the outer session instead.\n */\nconst registered: SessionDocumentValidator[] = []\n\n/** Publish the session's canonical document validator (`null` clears the stack). */\nexport function setSessionDocumentValidator(next: SessionDocumentValidator | null): void {\n if (next === null) {\n registered.length = 0\n return\n }\n const at = registered.indexOf(next)\n if (at >= 0) registered.splice(at, 1)\n registered.push(next)\n}\n\n/**\n * Remove this validator — the unmount race guard. Removing an INNER session\n * leaves the outer one in place; removing one that already left is a no-op (a\n * remount registers the new validator before the old effect's cleanup runs).\n */\nexport function clearSessionDocumentValidator(owned: SessionDocumentValidator): void {\n const at = registered.indexOf(owned)\n if (at >= 0) registered.splice(at, 1)\n}\n\n/**\n * The reader handed to `createOperations`. `undefined` = no session ⇒ the gate\n * falls back; see {@link SessionDocumentValidator} and the operations layer's\n * `DocumentValidator`.\n */\nexport function sessionValidateDocument(\n document: CmsDocument,\n): readonly CmsValidationIssue[] | undefined {\n return registered.at(-1)?.(document)\n}\n"]}
1
+ {"version":3,"file":"document-gate.js","sourceRoot":"","sources":["../../../src/lib/cms/document-gate.ts"],"names":[],"mappings":"AAgCA;;;;;;;;GAQG;AACH,MAAM,UAAU,GAA+B,EAAE,CAAA;AAEjD,wFAAwF;AACxF,MAAM,CAAC,MAAM,iCAAiC,GAAG,sCAAsC,CAAA;AAEvF;;;;;;;GAOG;AACH,MAAM,kBAAkB,GAAkC;IACxD;QACE,IAAI,EAAE,mBAAmB;QACzB,QAAQ,EAAE,OAAO;QACjB,OAAO,EAAE,qEAAqE;QAC9E,UAAU,EAAE,EAAE,GAAG,EAAE,iCAAiC,EAAE;QACtD,IAAI,EAAE,EAAE;KACT;CACF,CAAA;AAED,oFAAoF;AACpF,MAAM,UAAU,2BAA2B,CAAC,IAAqC;IAC/E,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAClB,UAAU,CAAC,MAAM,GAAG,CAAC,CAAA;QACrB,OAAM;IACR,CAAC;IACD,MAAM,EAAE,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IACnC,IAAI,EAAE,IAAI,CAAC;QAAE,UAAU,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;IACrC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACvB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,6BAA6B,CAAC,KAA+B;IAC3E,MAAM,EAAE,GAAG,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;IACpC,IAAI,EAAE,IAAI,CAAC;QAAE,UAAU,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;AACvC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,QAAqB;IAC3D,MAAM,OAAO,GAAG,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;IACjC,OAAO,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAA;AACzD,CAAC","sourcesContent":["import type { CmsDocument, CmsValidationIssue } from '@elytracms/core/cms-core'\n\n/**\n * The publish-gate seam (EC-497) — a framework-free module store, the same shape\n * as the current-document seam (`lib/ai-current-document`).\n *\n * `createOperations` is a module singleton built long before (and independently\n * of) any React tree, while gate-strength validation needs the live CMS session:\n * its document store, its asset oracle, its published-ness pointer. So the CMS\n * session PUBLISHES a validator here while mounted, and\n * `createStudioOperations` hands this module's reader to the operations layer\n * once, at bootstrap.\n *\n * **EC-546 — a gate that cannot see says so.** The first cut had the reader return\n * `undefined` with nothing registered, meaning \"no session context here\", and\n * the operations gate answered that by falling back to an optionless\n * `validateDocument` over the adapter's schema record: scalar rules only, no\n * store, no locale config, no composition or embed leash. Measured: a document\n * carrying a component outside its slot's `allow` published through\n * `createStudioOperations` whenever the stack was empty. That is the exact\n * weaker, mode-dependent gate EC-497 removed, kept alive by a mount-state side\n * channel. So the reader now answers with issues ONLY: with nothing registered it\n * returns one blocking `error` issue — the publish is refused and the refusal is\n * visible — and the operations layer no longer has a fallback to take (its\n * `DocumentValidator` cannot return `undefined`).\n *\n * This module imports nothing but types, so hanging the seam off it never drags\n * the CMS session (or the component registry behind the composition validators)\n * into the operations bootstrap's module graph.\n */\nexport type SessionDocumentValidator = (document: CmsDocument) => readonly CmsValidationIssue[]\n\n/**\n * Registered sessions, innermost LAST — a stack, not a single slot, because more\n * than one `CmsProvider` can be mounted at a time: the \"Use in document…\"\n * transfer dialog mounts its own over the app's\n * (`components/transfer-dialog.tsx:92`). With a single slot, closing that dialog\n * would clear the seam and silently drop the publish gate back to its fallback\n * for the rest of the app's life — a regression invisible until something\n * published. Popping restores the outer session instead.\n */\nconst registered: SessionDocumentValidator[] = []\n\n/** The catalog key of the refusal the reader emits with no session mounted (EC-546). */\nexport const NO_VALIDATION_SESSION_MESSAGE_KEY = 'cms.issueMessage.noValidationSession'\n\n/**\n * The verdict with no session mounted: one blocking issue, never a pass and never\n * `undefined`. `custom-validation` is the code for a rule the engine's field\n * vocabulary does not name — this is a host rule (\"publish only through a mounted\n * session\"), not a finding about the document — and the message carries the\n * meaning. Published via the same `messageRef` the engine's issues use, so the\n * studio's operation-error banner renders it in the active locale (EC-494).\n */\nconst NO_SESSION_REFUSAL: readonly CmsValidationIssue[] = [\n {\n code: 'custom-validation',\n severity: 'error',\n message: 'No validation session is mounted — refusing to publish unvalidated.',\n messageRef: { key: NO_VALIDATION_SESSION_MESSAGE_KEY },\n path: [],\n },\n]\n\n/** Publish the session's canonical document validator (`null` clears the stack). */\nexport function setSessionDocumentValidator(next: SessionDocumentValidator | null): void {\n if (next === null) {\n registered.length = 0\n return\n }\n const at = registered.indexOf(next)\n if (at >= 0) registered.splice(at, 1)\n registered.push(next)\n}\n\n/**\n * Remove this validator — the unmount race guard. Removing an INNER session\n * leaves the outer one in place; removing one that already left is a no-op (a\n * remount registers the new validator before the old effect's cleanup runs).\n */\nexport function clearSessionDocumentValidator(owned: SessionDocumentValidator): void {\n const at = registered.indexOf(owned)\n if (at >= 0) registered.splice(at, 1)\n}\n\n/**\n * The reader handed to `createOperations`. The innermost session's verdict, or —\n * with no session mounted — the blocking refusal described on\n * {@link SessionDocumentValidator}. Never `undefined`: the operations layer's\n * `DocumentValidator` has no fallback to offer and no longer accepts one (EC-546).\n */\nexport function sessionValidateDocument(document: CmsDocument): readonly CmsValidationIssue[] {\n const session = registered.at(-1)\n return session ? session(document) : NO_SESSION_REFUSAL\n}\n"]}
@@ -5,6 +5,7 @@ const PUBLISHED_RELOAD_OPS = [
5
5
  'documents.unpublish',
6
6
  'documents.restore',
7
7
  'documents.delete',
8
+ 'documents.clear',
8
9
  ];
9
10
  const EMPTY_POOL = new Map();
10
11
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"published-documents.js","sourceRoot":"","sources":["../../../src/lib/cms/published-documents.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAsB/D,6EAA6E;AAC7E,MAAM,oBAAoB,GAAG;IAC3B,mBAAmB;IACnB,qBAAqB;IACrB,mBAAmB;IACnB,kBAAkB;CACV,CAAA;AAEV,MAAM,UAAU,GAAqC,IAAI,GAAG,EAAE,CAAA;AAE9D;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,UAA4B,EAC5B,SAAiB,EACjB,YAAoB;IAEpB,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,SAAS,CAAC,aAAa,CAAC,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,CAAC,CAAA;IAChG,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,OAAO,IAAI,GAAG,EAAE,CAAA;IAChC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAuB,CAAA;IAC3C,KAAK,MAAM,QAAQ,IAAI,MAAM,CAAC,KAAK;QAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAA;IACpE,OAAO,IAAI,CAAA;AACb,CAAC;AAED,MAAM,UAAU,qBAAqB,CACnC,SAA6B,EAC7B,YAAoB;IAEpB,MAAM,KAAK,GAAG,iBAAiB,CAA2B;QACxD,4EAA4E;QAC5E,QAAQ,EAAE,CAAC,oBAAoB,EAAE,SAAS,IAAI,GAAG,EAAE,YAAY,CAAC;QAChE,MAAM,EAAE,CAAC,UAAU,EAAE,EAAE,CAAC,sBAAsB,CAAC,UAAU,EAAE,SAAmB,EAAE,YAAY,CAAC;QAC7F,YAAY,EAAE,oBAAoB;QAClC,OAAO,EAAE,SAAS,KAAK,SAAS;KACjC,CAAC,CAAA;IACF,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,CAAA;IACzE,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,EAAE,CAAA;AAC1F,CAAC","sourcesContent":["import type { CmsDocument } from '@elytracms/core/cms-core'\nimport type { OperationsClient } from '../operations'\nimport { useOperationQuery } from '@elytracms/studio/lib/query'\n\n/**\n * The PUBLISHED-perspective documents of a collection (EC-205, \"live = published\")\n * — the pinned published snapshot of each currently-published document, keyed by\n * id. Live surfaces (a document's live URL + \"View live\", EC-216) MUST derive\n * from this, never from the draft rows the studio edits: an unpublished document,\n * or an unpublished `slug`/`parent` edit, must never leak into a \"live\" URL. The\n * pool is exactly the set the live site would serve, so composing a nested page's\n * URL from these ancestors yields the URL the live site actually resolves.\n *\n * EC-308/AD-17: loaded + kept fresh through the one `useOperationQuery` cache\n * seam — invalidated only when a publish/unpublish/restore/delete command\n * succeeds (NOT on every autosave upsert — a draft edit doesn't change what's\n * published). Cost: ONE bulk `documents.listPublished` read (EC-307).\n */\nexport interface PublishedDocuments {\n /** Pinned published snapshot per published document id. Empty while loading. */\n byId: ReadonlyMap<string, CmsDocument>\n status: 'loading' | 'ready'\n}\n\n/** Commands that change which version is pinned live (and thus the pool). */\nconst PUBLISHED_RELOAD_OPS = [\n 'documents.publish',\n 'documents.unpublish',\n 'documents.restore',\n 'documents.delete',\n] as const\n\nconst EMPTY_POOL: ReadonlyMap<string, CmsDocument> = new Map()\n\n/**\n * Build the published pool of a collection (pure over the operations client): the\n * pinned published snapshot of every currently-published document, keyed by id.\n * EC-307: one bulk `documents.listPublished` read — the backend resolves each\n * `publishedVersion` pointer to its recorded snapshot — instead of a `list` plus a\n * per-document `listVersions` fan-out. Read failures degrade to an empty pool.\n */\nexport async function loadPublishedDocuments(\n operations: OperationsClient,\n projectId: string,\n collectionId: string,\n): Promise<Map<string, CmsDocument>> {\n const result = await operations.documents.listPublished({ projectId, collection: collectionId })\n if (!result.ok) return new Map()\n const byId = new Map<string, CmsDocument>()\n for (const snapshot of result.value) byId.set(snapshot.id, snapshot)\n return byId\n}\n\nexport function usePublishedDocuments(\n projectId: string | undefined,\n collectionId: string,\n): PublishedDocuments {\n const query = useOperationQuery<Map<string, CmsDocument>>({\n // Playground/fixture mode (no backend) is `enabled: false` → never queried.\n queryKey: ['publishedDocuments', projectId ?? '∅', collectionId],\n loader: (operations) => loadPublishedDocuments(operations, projectId as string, collectionId),\n invalidateOn: PUBLISHED_RELOAD_OPS,\n enabled: projectId !== undefined,\n })\n if (projectId === undefined) return { byId: EMPTY_POOL, status: 'ready' }\n return { byId: query.data ?? EMPTY_POOL, status: query.isPending ? 'loading' : 'ready' }\n}\n"]}
1
+ {"version":3,"file":"published-documents.js","sourceRoot":"","sources":["../../../src/lib/cms/published-documents.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAsB/D,6EAA6E;AAC7E,MAAM,oBAAoB,GAAG;IAC3B,mBAAmB;IACnB,qBAAqB;IACrB,mBAAmB;IACnB,kBAAkB;IAClB,iBAAiB;CACT,CAAA;AAEV,MAAM,UAAU,GAAqC,IAAI,GAAG,EAAE,CAAA;AAE9D;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,UAA4B,EAC5B,SAAiB,EACjB,YAAoB;IAEpB,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,SAAS,CAAC,aAAa,CAAC,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,CAAC,CAAA;IAChG,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,OAAO,IAAI,GAAG,EAAE,CAAA;IAChC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAuB,CAAA;IAC3C,KAAK,MAAM,QAAQ,IAAI,MAAM,CAAC,KAAK;QAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAA;IACpE,OAAO,IAAI,CAAA;AACb,CAAC;AAED,MAAM,UAAU,qBAAqB,CACnC,SAA6B,EAC7B,YAAoB;IAEpB,MAAM,KAAK,GAAG,iBAAiB,CAA2B;QACxD,4EAA4E;QAC5E,QAAQ,EAAE,CAAC,oBAAoB,EAAE,SAAS,IAAI,GAAG,EAAE,YAAY,CAAC;QAChE,MAAM,EAAE,CAAC,UAAU,EAAE,EAAE,CAAC,sBAAsB,CAAC,UAAU,EAAE,SAAmB,EAAE,YAAY,CAAC;QAC7F,YAAY,EAAE,oBAAoB;QAClC,OAAO,EAAE,SAAS,KAAK,SAAS;KACjC,CAAC,CAAA;IACF,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,CAAA;IACzE,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,EAAE,CAAA;AAC1F,CAAC","sourcesContent":["import type { CmsDocument } from '@elytracms/core/cms-core'\nimport type { OperationsClient } from '../operations'\nimport { useOperationQuery } from '@elytracms/studio/lib/query'\n\n/**\n * The PUBLISHED-perspective documents of a collection (EC-205, \"live = published\")\n * — the pinned published snapshot of each currently-published document, keyed by\n * id. Live surfaces (a document's live URL + \"View live\", EC-216) MUST derive\n * from this, never from the draft rows the studio edits: an unpublished document,\n * or an unpublished `slug`/`parent` edit, must never leak into a \"live\" URL. The\n * pool is exactly the set the live site would serve, so composing a nested page's\n * URL from these ancestors yields the URL the live site actually resolves.\n *\n * EC-308/AD-17: loaded + kept fresh through the one `useOperationQuery` cache\n * seam — invalidated only when a publish/unpublish/restore/delete command\n * succeeds (NOT on every autosave upsert — a draft edit doesn't change what's\n * published). Cost: ONE bulk `documents.listPublished` read (EC-307).\n */\nexport interface PublishedDocuments {\n /** Pinned published snapshot per published document id. Empty while loading. */\n byId: ReadonlyMap<string, CmsDocument>\n status: 'loading' | 'ready'\n}\n\n/** Commands that change which version is pinned live (and thus the pool). */\nconst PUBLISHED_RELOAD_OPS = [\n 'documents.publish',\n 'documents.unpublish',\n 'documents.restore',\n 'documents.delete',\n 'documents.clear',\n] as const\n\nconst EMPTY_POOL: ReadonlyMap<string, CmsDocument> = new Map()\n\n/**\n * Build the published pool of a collection (pure over the operations client): the\n * pinned published snapshot of every currently-published document, keyed by id.\n * EC-307: one bulk `documents.listPublished` read — the backend resolves each\n * `publishedVersion` pointer to its recorded snapshot — instead of a `list` plus a\n * per-document `listVersions` fan-out. Read failures degrade to an empty pool.\n */\nexport async function loadPublishedDocuments(\n operations: OperationsClient,\n projectId: string,\n collectionId: string,\n): Promise<Map<string, CmsDocument>> {\n const result = await operations.documents.listPublished({ projectId, collection: collectionId })\n if (!result.ok) return new Map()\n const byId = new Map<string, CmsDocument>()\n for (const snapshot of result.value) byId.set(snapshot.id, snapshot)\n return byId\n}\n\nexport function usePublishedDocuments(\n projectId: string | undefined,\n collectionId: string,\n): PublishedDocuments {\n const query = useOperationQuery<Map<string, CmsDocument>>({\n // Playground/fixture mode (no backend) is `enabled: false` → never queried.\n queryKey: ['publishedDocuments', projectId ?? '∅', collectionId],\n loader: (operations) => loadPublishedDocuments(operations, projectId as string, collectionId),\n invalidateOn: PUBLISHED_RELOAD_OPS,\n enabled: projectId !== undefined,\n })\n if (projectId === undefined) return { byId: EMPTY_POOL, status: 'ready' }\n return { byId: query.data ?? EMPTY_POOL, status: query.isPending ? 'loading' : 'ready' }\n}\n"]}
@@ -38,8 +38,8 @@ export type FilterClause = z.infer<typeof filterClauseSchema>;
38
38
  export declare const viewSortSchema: z.ZodObject<{
39
39
  field: z.ZodString;
40
40
  dir: z.ZodEnum<{
41
- asc: "asc";
42
41
  desc: "desc";
42
+ asc: "asc";
43
43
  }>;
44
44
  }, z.core.$strip>;
45
45
  export type ViewSort = z.infer<typeof viewSortSchema>;
@@ -63,8 +63,8 @@ export declare const tableViewStateSchema: z.ZodObject<{
63
63
  sort: z.ZodNullable<z.ZodObject<{
64
64
  field: z.ZodString;
65
65
  dir: z.ZodEnum<{
66
- asc: "asc";
67
66
  desc: "desc";
67
+ asc: "asc";
68
68
  }>;
69
69
  }, z.core.$strip>>;
70
70
  columns: z.ZodArray<z.ZodString>;
@@ -305,8 +305,8 @@ export declare const configViewSchema: z.ZodObject<{
305
305
  sort: z.ZodOptional<z.ZodNullable<z.ZodObject<{
306
306
  field: z.ZodString;
307
307
  dir: z.ZodEnum<{
308
- asc: "asc";
309
308
  desc: "desc";
309
+ asc: "asc";
310
310
  }>;
311
311
  }, z.core.$strip>>>;
312
312
  columns: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -1,6 +1,3 @@
1
- export * from './data-store.js';
2
- export * from './sample-capture.js';
3
- export * from './sources-store.js';
4
1
  export * from './collection-content.js';
5
2
  export * from './content-session-refresh.js';
6
3
  export * from './playground-unload-guard.js';
@@ -1,10 +1,8 @@
1
- export * from './data-store.js';
2
- export * from './sample-capture.js';
3
- export * from './sources-store.js';
1
+ // EC-574: the data-source store, sample capture, providers and sources-store that
2
+ // used to live here went with the data-binding subsystem. What remains is the
3
+ // canvas's content session (collection-content), its write-sync refresh and the
4
+ // playground unload guard.
4
5
  export * from './collection-content.js';
5
6
  export * from './content-session-refresh.js';
6
7
  export * from './playground-unload-guard.js';
7
- // Data providers (EC-167) are imported from './providers' directly: the module
8
- // exports a `DataProvider` domain type that would collide with the store's
9
- // `<DataProvider>` React provider in a wildcard barrel.
10
8
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/data/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,sBAAsB,CAAA;AACpC,cAAc,2BAA2B,CAAA;AACzC,cAAc,2BAA2B,CAAA;AACzC,+EAA+E;AAC/E,2EAA2E;AAC3E,wDAAwD","sourcesContent":["export * from './data-store'\nexport * from './sample-capture'\nexport * from './sources-store'\nexport * from './collection-content'\nexport * from './content-session-refresh'\nexport * from './playground-unload-guard'\n// Data providers (EC-167) are imported from './providers' directly: the module\n// exports a `DataProvider` domain type that would collide with the store's\n// `<DataProvider>` React provider in a wildcard barrel.\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/data/index.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,8EAA8E;AAC9E,gFAAgF;AAChF,2BAA2B;AAC3B,cAAc,sBAAsB,CAAA;AACpC,cAAc,2BAA2B,CAAA;AACzC,cAAc,2BAA2B,CAAA","sourcesContent":["// EC-574: the data-source store, sample capture, providers and sources-store that\n// used to live here went with the data-binding subsystem. What remains is the\n// canvas's content session (collection-content), its write-sync refresh and the\n// playground unload guard.\nexport * from './collection-content'\nexport * from './content-session-refresh'\nexport * from './playground-unload-guard'\n"]}
@@ -4,8 +4,9 @@ import type { ComponentManifest } from '@elytracms/core/component-registry';
4
4
  * Drag & drop eligibility rules (EC-160 + EC-162, vision §3.1) — pure
5
5
  * graph-level functions with **no DOM or UI dependencies**. The canvas and the
6
6
  * page tree both resolve drops through `resolveDrop`, so the two surfaces can
7
- * never disagree about what a manifest slot allow-list permits, which children
8
- * a component accepts, or whether a move would create a cycle.
7
+ * never disagree about what a manifest slot allow-list permits, what the
8
+ * composition field's top-level vocabulary permits at the root (EC-554), which
9
+ * children a component accepts, or whether a move would create a cycle.
9
10
  *
10
11
  * The drop-zone model is position-based: `into` a slot, `before`/`after` a
11
12
  * sibling along the parent's flow axis, and the EC-162 gesture positions
@@ -72,6 +73,11 @@ export type DropRejectionCode =
72
73
  | 'no-slot'
73
74
  /** The target slot's manifest allow-list rejects the dragged component. */
74
75
  | 'not-allowed'
76
+ /**
77
+ * EC-554: the composition FIELD's top-level `allow` rejects the component
78
+ * landing as a direct child of the composition root (see {@link fieldAllowAt}).
79
+ */
80
+ | 'field-not-allowed'
75
81
  /** Moving a node into itself or its own subtree (cycle). */
76
82
  | 'own-subtree'
77
83
  /** The page root cannot be moved, and nothing can drop beside it. */
@@ -119,6 +125,37 @@ export declare function primaryInsertSlot(manifest: ManifestRules | undefined):
119
125
  export declare function canNestInto(getManifest: ManifestLookup, componentId: string): boolean;
120
126
  /** True when a slot's allow-list (if any) permits the component id. */
121
127
  export declare function slotAllows(slot: SlotRule | undefined, componentId: string): boolean;
128
+ /**
129
+ * The composition FIELD's top-level vocabulary as it applies to a placement under
130
+ * `parentId` (EC-188, EC-554) — the ONE reader of a field's `allow` on the studio
131
+ * side: every insertion surface (the insert offer in `insert-context`, drag-and-drop
132
+ * on the canvas and the page tree incl. the wrap gesture, paste) asks here, so no
133
+ * surface can apply the field leash differently or not at all.
134
+ *
135
+ * A field `allow` governs only the value's TOP level — a direct child of the
136
+ * composition root, mirroring `validateCompositionValue` — so this returns the
137
+ * field vocabulary when `parentId` IS the root node and `undefined` (no field
138
+ * constraint) anywhere deeper. Keyed on node IDENTITY, never on the component id:
139
+ * the composition editor's synthetic root is a `base.Stack`, which is also a
140
+ * nestable block, and a rule keyed on the component id would leash every nested
141
+ * Stack. EC-544: an omitted `allow` is `undefined` here (any registered
142
+ * component); a declared list is a closed set.
143
+ */
144
+ export declare function fieldAllowAt(root: ComponentNode, parentId: string, fieldAllow: readonly string[] | undefined): readonly string[] | undefined;
145
+ /**
146
+ * Which declared leash refuses `componentId` landing under `parentId` in `slot`
147
+ * (EC-545 slot leash, EC-554 field leash) — `null` when both admit it. The field
148
+ * is judged first: it can only refuse at the composition root, where the slot is
149
+ * the invisible synthetic root's and the field is the declaration the author sees.
150
+ */
151
+ export declare function placementRefusal(args: {
152
+ root: ComponentNode;
153
+ /** The node the placement lands under (the insertion target's `parentId`). */
154
+ parentId: string;
155
+ slot: SlotRule | undefined;
156
+ fieldAllow: readonly string[] | undefined;
157
+ componentId: string;
158
+ }): 'field' | 'slot' | null;
122
159
  /**
123
160
  * Resolve a prospective drop into either an executable plan (one `move` or
124
161
  * `insert` builder action → one operations command → one undo step) or a
@@ -128,6 +165,12 @@ export declare function slotAllows(slot: SlotRule | undefined, componentId: stri
128
165
  export declare function resolveDrop(args: {
129
166
  root: ComponentNode;
130
167
  getManifest: ManifestLookup;
168
+ /**
169
+ * The composition field's top-level `allow` (EC-554) — `undefined` when the
170
+ * field declares none. Required so no surface can forget to pass it; judged
171
+ * through {@link fieldAllowAt} for drops whose parent is the root.
172
+ */
173
+ fieldAllow: readonly string[] | undefined;
131
174
  subject: DragSubject;
132
175
  location: DropLocation;
133
176
  }): DropResolution;