@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.
- package/convex/asset-revalidate.test.ts +186 -0
- package/convex/assets.ts +53 -19
- package/convex/cms.ts +22 -0
- package/convex/delivery.ts +54 -116
- package/convex/engine-version.test.ts +31 -0
- package/convex/functions.ts +3 -3
- package/convex/rail-write-parity.test.ts +180 -0
- package/convex/redirects.ts +33 -11
- package/convex/revalidate.ts +7 -5
- package/convex/triggers.ts +53 -3
- package/dist/components/ai/ai-chat-panel.js +13 -2
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/app-views/default-modules.d.ts +20 -20
- package/dist/components/app-views/default-modules.js +27 -48
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/app-views/studio-config.d.ts +5 -6
- package/dist/components/app-views/studio-config.js.map +1 -1
- package/dist/components/assets/asset-metadata.d.ts +28 -19
- package/dist/components/assets/asset-metadata.js +24 -31
- package/dist/components/assets/asset-metadata.js.map +1 -1
- package/dist/components/assets/asset-selection.d.ts +24 -5
- package/dist/components/assets/asset-selection.js +39 -9
- package/dist/components/assets/asset-selection.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +17 -14
- package/dist/components/assets/assets-collection-view-source.js +25 -31
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/builder/blocks-model.d.ts +5 -4
- package/dist/components/builder/blocks-model.js +5 -4
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/blocks-panel.js +3 -3
- package/dist/components/builder/blocks-panel.js.map +1 -1
- package/dist/components/builder/builder-store.d.ts +25 -1
- package/dist/components/builder/builder-store.js +3 -2
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/canvas-chrome-css.js +0 -17
- package/dist/components/builder/canvas-chrome-css.js.map +1 -1
- package/dist/components/builder/canvas-chrome.d.ts +10 -12
- package/dist/components/builder/canvas-chrome.js +12 -13
- package/dist/components/builder/canvas-chrome.js.map +1 -1
- package/dist/components/builder/canvas-dnd.d.ts +8 -0
- package/dist/components/builder/canvas-dnd.js +5 -3
- package/dist/components/builder/canvas-dnd.js.map +1 -1
- package/dist/components/builder/canvas-render-context.d.ts +12 -24
- package/dist/components/builder/canvas-render-context.js +15 -44
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/canvas.js +5 -11
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/clipboard-actions.d.ts +15 -1
- package/dist/components/builder/clipboard-actions.js +35 -12
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor.d.ts +32 -26
- package/dist/components/builder/composition-field-editor.js +47 -54
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/insert-context.d.ts +3 -1
- package/dist/components/builder/insert-context.js +19 -12
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/page-tree.d.ts +29 -0
- package/dist/components/builder/page-tree.js +56 -58
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/reuse.js +5 -2
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/cms/asset-field.js +20 -32
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/cell-edit.d.ts +7 -0
- package/dist/components/cms/cell-edit.js +9 -1
- package/dist/components/cms/cell-edit.js.map +1 -1
- package/dist/components/cms/cms-composition-validators.js +11 -2
- package/dist/components/cms/cms-composition-validators.js.map +1 -1
- package/dist/components/cms/collection-table.js +11 -0
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/document-url-path.d.ts +7 -4
- package/dist/components/cms/document-url-path.js +15 -1
- package/dist/components/cms/document-url-path.js.map +1 -1
- package/dist/components/cms/editable-cell.js +5 -2
- package/dist/components/cms/editable-cell.js.map +1 -1
- package/dist/components/cms/field-control.js +4 -0
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/inline-field-errors.d.ts +36 -0
- package/dist/components/cms/inline-field-errors.js +13 -5
- package/dist/components/cms/inline-field-errors.js.map +1 -1
- package/dist/components/cms/rich-text/insert.d.ts +6 -3
- package/dist/components/cms/rich-text/insert.js +9 -6
- package/dist/components/cms/rich-text/insert.js.map +1 -1
- package/dist/components/cms/use-document-versions.js +1 -0
- package/dist/components/cms/use-document-versions.js.map +1 -1
- package/dist/components/cms/use-live-url.d.ts +18 -6
- package/dist/components/cms/use-live-url.js +10 -0
- package/dist/components/cms/use-live-url.js.map +1 -1
- package/dist/components/cms/versions-model.d.ts +0 -12
- package/dist/components/cms/versions-model.js +3 -15
- package/dist/components/cms/versions-model.js.map +1 -1
- package/dist/components/components/component-detail.js +1 -1
- package/dist/components/components/component-detail.js.map +1 -1
- package/dist/components/components/transfer-dialog.js +2 -4
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/components/transfer.d.ts +19 -15
- package/dist/components/components/transfer.js +28 -25
- package/dist/components/components/transfer.js.map +1 -1
- package/dist/components/deployment-setup-gate.js +9 -3
- package/dist/components/deployment-setup-gate.js.map +1 -1
- package/dist/components/project-session.d.ts +9 -0
- package/dist/components/project-session.js +1 -1
- package/dist/components/project-session.js.map +1 -1
- package/dist/components/redirects/redirects-collection-view-source.d.ts +17 -2
- package/dist/components/redirects/redirects-collection-view-source.js +20 -6
- package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
- package/dist/components/section-surface.js +3 -2
- package/dist/components/section-surface.js.map +1 -1
- package/dist/components/settings-form.js +1 -2
- package/dist/components/settings-form.js.map +1 -1
- package/dist/components/studio-context.js +1 -18
- package/dist/components/studio-context.js.map +1 -1
- package/dist/components/studio-project-screen.js +3 -5
- package/dist/components/studio-project-screen.js.map +1 -1
- package/dist/lib/ai/apply-content.js +73 -29
- package/dist/lib/ai/apply-content.js.map +1 -1
- package/dist/lib/ai/constraints.d.ts +35 -0
- package/dist/lib/ai/constraints.js +155 -72
- package/dist/lib/ai/constraints.js.map +1 -1
- package/dist/lib/ai/index.d.ts +1 -0
- package/dist/lib/ai/index.js +1 -0
- package/dist/lib/ai/index.js.map +1 -1
- package/dist/lib/ai/route-fetch.d.ts +58 -0
- package/dist/lib/ai/route-fetch.js +98 -0
- package/dist/lib/ai/route-fetch.js.map +1 -0
- package/dist/lib/ai/route-provider.d.ts +5 -22
- package/dist/lib/ai/route-provider.js +23 -1
- package/dist/lib/ai/route-provider.js.map +1 -1
- package/dist/lib/ai/route.js +4 -1
- package/dist/lib/ai/route.js.map +1 -1
- package/dist/lib/ai/shape.d.ts +3 -2
- package/dist/lib/ai/shape.js +16 -24
- package/dist/lib/ai/shape.js.map +1 -1
- package/dist/lib/ai/types.d.ts +8 -0
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.js +33 -12
- package/dist/lib/ai/update-fields.js.map +1 -1
- package/dist/lib/assets/asset-placements.d.ts +35 -0
- package/dist/lib/assets/asset-placements.js +74 -0
- package/dist/lib/assets/asset-placements.js.map +1 -0
- package/dist/lib/assets/backfill-thumbs.d.ts +9 -8
- package/dist/lib/assets/backfill-thumbs.js +13 -25
- package/dist/lib/assets/backfill-thumbs.js.map +1 -1
- package/dist/lib/clipboard/paste-issues.d.ts +17 -15
- package/dist/lib/clipboard/paste-issues.js +8 -45
- package/dist/lib/clipboard/paste-issues.js.map +1 -1
- package/dist/lib/clipboard/paste-target.d.ts +47 -1
- package/dist/lib/clipboard/paste-target.js +19 -11
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/cms/document-gate.d.ts +19 -9
- package/dist/lib/cms/document-gate.js +25 -4
- package/dist/lib/cms/document-gate.js.map +1 -1
- package/dist/lib/cms/published-documents.js +1 -0
- package/dist/lib/cms/published-documents.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +3 -3
- package/dist/lib/data/index.d.ts +0 -3
- package/dist/lib/data/index.js +4 -6
- package/dist/lib/data/index.js.map +1 -1
- package/dist/lib/dnd/drop-rules.d.ts +45 -2
- package/dist/lib/dnd/drop-rules.js +64 -9
- package/dist/lib/dnd/drop-rules.js.map +1 -1
- package/dist/lib/engine-version.d.ts +16 -0
- package/dist/lib/engine-version.js +17 -0
- package/dist/lib/engine-version.js.map +1 -0
- package/dist/lib/i18n/messages.de.js +8 -56
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +8 -53
- package/dist/lib/i18n/messages.en.js +15 -57
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/locale/fallback.js +8 -2
- package/dist/lib/locale/fallback.js.map +1 -1
- package/dist/lib/locale/index.d.ts +0 -1
- package/dist/lib/locale/index.js +0 -1
- package/dist/lib/locale/index.js.map +1 -1
- package/dist/lib/locale/locale-store.d.ts +10 -3
- package/dist/lib/locale/locale-store.js +12 -5
- package/dist/lib/locale/locale-store.js.map +1 -1
- package/dist/lib/operations/session-hydration.d.ts +9 -5
- package/dist/lib/operations/session-hydration.js +10 -10
- package/dist/lib/operations/session-hydration.js.map +1 -1
- package/dist/lib/operations/studio-operations.js +4 -2
- package/dist/lib/operations/studio-operations.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.js +63 -75
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persistence/deployment-readiness.d.ts +8 -1
- package/dist/lib/persistence/deployment-readiness.js +4 -0
- package/dist/lib/persistence/deployment-readiness.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +0 -2
- package/dist/lib/persona/labels.js +1 -7
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/registry/index.d.ts +4 -2
- package/dist/lib/registry/index.js +26 -21
- package/dist/lib/registry/index.js.map +1 -1
- package/dist/lib/registry/project-components.js +0 -3
- package/dist/lib/registry/project-components.js.map +1 -1
- package/dist/lib/routes/route-issues.js +4 -5
- package/dist/lib/routes/route-issues.js.map +1 -1
- package/dist/lib/theme.js +3 -2
- package/dist/lib/theme.js.map +1 -1
- package/dist/lib/webhooks/tags.js +11 -14
- package/dist/lib/webhooks/tags.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +1 -1
- package/dist/lib/workspace/index.js +1 -1
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/previews/enumerate.d.ts +27 -9
- package/dist/previews/enumerate.js +27 -6
- package/dist/previews/enumerate.js.map +1 -1
- package/dist/previews/manifest-format.d.ts +16 -7
- package/dist/previews/manifest-format.js +0 -21
- package/dist/previews/manifest-format.js.map +1 -1
- package/dist/router/match.d.ts +0 -2
- package/dist/router/match.js +0 -2
- package/dist/router/match.js.map +1 -1
- package/dist/ui/badge.d.ts +1 -1
- package/package.json +2 -2
- package/src/styles/studio.css +5 -4
- package/src/styles/tokens.css +72 -55
- package/convex/content.ts +0 -291
- package/dist/components/collab/collab-context.d.ts +0 -27
- package/dist/components/collab/collab-context.js +0 -39
- package/dist/components/collab/collab-context.js.map +0 -1
- package/dist/components/collab/collab-seed.d.ts +0 -15
- package/dist/components/collab/collab-seed.js +0 -20
- package/dist/components/collab/collab-seed.js.map +0 -1
- package/dist/components/extensions/extensions-view.d.ts +0 -7
- package/dist/components/extensions/extensions-view.js +0 -127
- package/dist/components/extensions/extensions-view.js.map +0 -1
- package/dist/lib/bindings/section-bindings.d.ts +0 -24
- package/dist/lib/bindings/section-bindings.js +0 -9
- package/dist/lib/bindings/section-bindings.js.map +0 -1
- package/dist/lib/data/data-store.d.ts +0 -98
- package/dist/lib/data/data-store.js +0 -211
- package/dist/lib/data/data-store.js.map +0 -1
- package/dist/lib/data/providers.d.ts +0 -143
- package/dist/lib/data/providers.js +0 -255
- package/dist/lib/data/providers.js.map +0 -1
- package/dist/lib/data/sample-capture.d.ts +0 -30
- package/dist/lib/data/sample-capture.js +0 -70
- package/dist/lib/data/sample-capture.js.map +0 -1
- package/dist/lib/data/sources-store.d.ts +0 -42
- package/dist/lib/data/sources-store.js +0 -120
- package/dist/lib/data/sources-store.js.map +0 -1
- package/dist/lib/locale/binding-fallback.d.ts +0 -35
- package/dist/lib/locale/binding-fallback.js +0 -73
- package/dist/lib/locale/binding-fallback.js.map +0 -1
- package/dist/lib/starters/kit-session.d.ts +0 -5
- package/dist/lib/starters/kit-session.js +0 -17
- 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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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-
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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'
|
|
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
|
|
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 —
|
|
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
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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 —
|
|
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":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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;
|
|
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,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAA;AAEzG,OAAO,EAAE,OAAO,EAAE,MAAM,sCAAsC,CAAA;AA2D9D,MAAM,UAAU,kBAAkB,CAAC,IAYlC;IACC,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,GAAG,IAAI,CAAA;IAC3D,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,MAAM,OAAO,GAAG,CACd,IAAY,EACZ,QAA8B,EAC9B,MAAoB,EACpB,gBAAwB,EACP,EAAE;QACnB,MAAM,OAAO,GAAG,gBAAgB,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC,CAAA;QAC9G,IAAI,OAAO,KAAK,OAAO;YAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,mBAAmB,EAAE,WAAW,EAAE,CAAA;QACrF,IAAI,OAAO,KAAK,MAAM;YAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,WAAW,EAAE,IAAI,EAAE,gBAAgB,EAAE,CAAA;QACtG,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAA;IAC/C,CAAC,CAAA;IAED,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,OAAO,CACZ,SAAS,CAAC,IAAI,EACd,SAAS,EACT,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE,EAC3C,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,SAAS,CAAC,IAAI,EAAE,CACnD,CAAA;IACH,CAAC;IAED,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,CAAA;IACzC,IAAI,IAAI,EAAE,CAAC;QACT,0EAA0E;QAC1E,wEAAwE;QACxE,8BAA8B;QAC9B,MAAM,UAAU,GAAG,aAAa,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,CAAA;QAC/G,OAAO,OAAO,CACZ,IAAI,CAAC,IAAI,EACT,UAAU,EACV,EAAE,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC,EAAE,EACpE,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CACrD,CAAA;IACH,CAAC;IAED,8EAA8E;IAC9E,+EAA+E;IAC/E,WAAW;IACX,OAAO,OAAO,CAAC,UAAU,EAAE,SAAS,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,MAAM,CAAC,CAAA;AACxF,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, 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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* the operations gate
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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`.
|
|
34
|
-
*
|
|
35
|
-
* `
|
|
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[]
|
|
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`.
|
|
34
|
-
*
|
|
35
|
-
* `
|
|
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
|
-
|
|
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":"
|
|
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"]}
|
|
@@ -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;
|
|
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>>;
|
package/dist/lib/data/index.d.ts
CHANGED
package/dist/lib/data/index.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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,
|
|
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,
|
|
8
|
-
*
|
|
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;
|