@elytracms/studio 0.0.91 → 0.0.93
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-alt-migration.test.ts +127 -0
- package/convex/asset-replace.test.ts +168 -0
- package/convex/asset-revalidate.test.ts +6 -6
- package/convex/assets.ts +41 -5
- package/convex/cms.ts +537 -177
- package/convex/contract-parity.test.ts +173 -19
- package/convex/delivery.ts +26 -13
- package/convex/document-revalidate.test.ts +17 -16
- package/convex/orphan-hydration.test.ts +116 -0
- package/convex/presence-cost.test.ts +89 -0
- package/convex/rail-write-parity.test.ts +17 -14
- package/convex/referenceMaintenance.ts +15 -15
- package/convex/schema.ts +70 -5
- package/convex/triggers.ts +21 -8
- package/convex/write-limits.ts +1 -1
- package/dist/components/ai/ai-chat-panel.js +33 -22
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/ai/proposal-card.js +2 -1
- package/dist/components/ai/proposal-card.js.map +1 -1
- package/dist/components/ai/translate-document-action.d.ts +8 -2
- package/dist/components/ai/translate-document-action.js.map +1 -1
- package/dist/components/ai/translate-document-dialog.d.ts +1 -1
- package/dist/components/ai/translate-document-dialog.js +14 -8
- package/dist/components/ai/translate-document-dialog.js.map +1 -1
- package/dist/components/ai/translate-skipped.d.ts +19 -0
- package/dist/components/ai/translate-skipped.js +46 -0
- package/dist/components/ai/translate-skipped.js.map +1 -0
- package/dist/components/app-views/default-modules.d.ts +23 -6
- package/dist/components/app-views/default-modules.js +33 -14
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/assets/asset-detail.js +18 -8
- package/dist/components/assets/asset-detail.js.map +1 -1
- package/dist/components/assets/asset-metadata.d.ts +15 -4
- package/dist/components/assets/asset-metadata.js +15 -2
- package/dist/components/assets/asset-metadata.js.map +1 -1
- package/dist/components/assets/asset-search.js +4 -1
- package/dist/components/assets/asset-search.js.map +1 -1
- package/dist/components/assets/asset-selection.d.ts +0 -8
- package/dist/components/assets/asset-selection.js +4 -23
- package/dist/components/assets/asset-selection.js.map +1 -1
- package/dist/components/assets/asset-thumb.js +3 -2
- package/dist/components/assets/asset-thumb.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +3 -3
- package/dist/components/assets/assets-collection-view-source.js +9 -5
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/assets/assets-store.d.ts +14 -0
- package/dist/components/assets/assets-store.js +23 -1
- package/dist/components/assets/assets-store.js.map +1 -1
- package/dist/components/assets/replace-file.d.ts +22 -0
- package/dist/components/assets/replace-file.js +59 -0
- package/dist/components/assets/replace-file.js.map +1 -0
- package/dist/components/builder/blocks-model.d.ts +14 -26
- package/dist/components/builder/blocks-model.js +35 -41
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/blocks-panel.d.ts +7 -3
- package/dist/components/builder/blocks-panel.js +6 -5
- package/dist/components/builder/blocks-panel.js.map +1 -1
- package/dist/components/builder/builder-store.d.ts +126 -30
- package/dist/components/builder/builder-store.js +169 -67
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/canvas-body.d.ts +30 -0
- package/dist/components/builder/canvas-body.js +38 -0
- package/dist/components/builder/canvas-body.js.map +1 -0
- package/dist/components/builder/canvas-chrome-css.js +7 -0
- package/dist/components/builder/canvas-chrome-css.js.map +1 -1
- package/dist/components/builder/canvas-dnd.d.ts +7 -7
- package/dist/components/builder/canvas-dnd.js +5 -5
- package/dist/components/builder/canvas-dnd.js.map +1 -1
- package/dist/components/builder/canvas-render-context.d.ts +30 -22
- package/dist/components/builder/canvas-render-context.js +44 -44
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/canvas-runs.d.ts +45 -0
- package/dist/components/builder/canvas-runs.js +51 -0
- package/dist/components/builder/canvas-runs.js.map +1 -0
- package/dist/components/builder/canvas.d.ts +5 -4
- package/dist/components/builder/canvas.js +34 -6
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/clipboard-actions.d.ts +15 -0
- package/dist/components/builder/clipboard-actions.js +50 -13
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor.d.ts +10 -16
- package/dist/components/builder/composition-field-editor.js +30 -40
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/composition-patches.d.ts +7 -0
- package/dist/components/builder/composition-patches.js +256 -0
- package/dist/components/builder/composition-patches.js.map +1 -0
- package/dist/components/builder/delete-key.d.ts +5 -0
- package/dist/components/builder/delete-key.js +4 -0
- package/dist/components/builder/delete-key.js.map +1 -1
- package/dist/components/builder/insert-context.d.ts +17 -25
- package/dist/components/builder/insert-context.js +45 -92
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/page-tree.d.ts +3 -3
- package/dist/components/builder/page-tree.js +3 -3
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/placement-refusal.d.ts +18 -0
- package/dist/components/builder/placement-refusal.js +46 -0
- package/dist/components/builder/placement-refusal.js.map +1 -0
- package/dist/components/builder/property-inspector.js +4 -0
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/builder/reuse-context.js +65 -26
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/reuse.d.ts +79 -8
- package/dist/components/builder/reuse.js +71 -23
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/builder/run-inspector.d.ts +11 -0
- package/dist/components/builder/run-inspector.js +33 -0
- package/dist/components/builder/run-inspector.js.map +1 -0
- package/dist/components/cms/asset-field.d.ts +38 -1
- package/dist/components/cms/asset-field.js +182 -58
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/asset-picker-core.d.ts +16 -0
- package/dist/components/cms/asset-picker-core.js +20 -0
- package/dist/components/cms/asset-picker-core.js.map +1 -1
- package/dist/components/cms/asset-picker.js +12 -18
- package/dist/components/cms/asset-picker.js.map +1 -1
- package/dist/components/cms/bulk-actions.d.ts +4 -20
- package/dist/components/cms/bulk-actions.js +3 -24
- package/dist/components/cms/bulk-actions.js.map +1 -1
- package/dist/components/cms/cell-edit.d.ts +5 -5
- package/dist/components/cms/cell-edit.js.map +1 -1
- package/dist/components/cms/cell.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +87 -45
- package/dist/components/cms/cms-store.js +347 -203
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +7 -2
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/commit-policy.d.ts +24 -0
- package/dist/components/cms/commit-policy.js +37 -0
- package/dist/components/cms/commit-policy.js.map +1 -0
- package/dist/components/cms/detail-editor.js +83 -137
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/document-diff-view.d.ts +3 -1
- package/dist/components/cms/document-diff-view.js +12 -7
- package/dist/components/cms/document-diff-view.js.map +1 -1
- package/dist/components/cms/draft-commit.d.ts +70 -0
- package/dist/components/cms/draft-commit.js +82 -0
- package/dist/components/cms/draft-commit.js.map +1 -0
- package/dist/components/cms/duplicate-document.d.ts +11 -13
- package/dist/components/cms/duplicate-document.js +26 -34
- package/dist/components/cms/duplicate-document.js.map +1 -1
- package/dist/components/cms/field-anchors.d.ts +7 -4
- package/dist/components/cms/field-anchors.js +9 -7
- package/dist/components/cms/field-anchors.js.map +1 -1
- package/dist/components/cms/field-collaboration.d.ts +47 -0
- package/dist/components/cms/field-collaboration.js +87 -0
- package/dist/components/cms/field-collaboration.js.map +1 -0
- package/dist/components/cms/field-control-lib.d.ts +22 -1
- package/dist/components/cms/field-control-lib.js +33 -4
- package/dist/components/cms/field-control-lib.js.map +1 -1
- package/dist/components/cms/field-control.js +17 -14
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/gate-validation.d.ts +21 -30
- package/dist/components/cms/gate-validation.js +11 -22
- package/dist/components/cms/gate-validation.js.map +1 -1
- package/dist/components/cms/object-array-field.js +4 -4
- package/dist/components/cms/object-array-field.js.map +1 -1
- package/dist/components/cms/prop-fields.d.ts +7 -2
- package/dist/components/cms/prop-fields.js +13 -6
- package/dist/components/cms/prop-fields.js.map +1 -1
- package/dist/components/cms/reference-confirm.js +10 -4
- package/dist/components/cms/reference-confirm.js.map +1 -1
- package/dist/components/cms/rich-text/embed-picker.d.ts +1 -1
- package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
- package/dist/components/cms/rich-text/insert.d.ts +20 -9
- package/dist/components/cms/rich-text/insert.js +25 -17
- package/dist/components/cms/rich-text/insert.js.map +1 -1
- package/dist/components/cms/use-document-draft.d.ts +96 -0
- package/dist/components/cms/use-document-draft.js +283 -0
- package/dist/components/cms/use-document-draft.js.map +1 -0
- package/dist/components/cms/use-document-versions.js +2 -2
- package/dist/components/cms/use-document-versions.js.map +1 -1
- package/dist/components/cms/use-live-url.d.ts +6 -12
- package/dist/components/cms/use-live-url.js +16 -15
- package/dist/components/cms/use-live-url.js.map +1 -1
- package/dist/components/cms/use-publish-controller.d.ts +13 -18
- package/dist/components/cms/use-publish-controller.js +24 -41
- package/dist/components/cms/use-publish-controller.js.map +1 -1
- package/dist/components/cms/use-published-perspective.d.ts +2 -3
- package/dist/components/cms/use-published-perspective.js +4 -3
- package/dist/components/cms/use-published-perspective.js.map +1 -1
- package/dist/components/cms/versions-pane.js +8 -1
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/collection-view/cms-collection-view-source.js +24 -7
- package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
- package/dist/components/components/transfer-dialog.js +28 -23
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/components/transfer.d.ts +31 -21
- package/dist/components/components/transfer.js +49 -35
- package/dist/components/components/transfer.js.map +1 -1
- package/dist/components/project-list.js.map +1 -1
- package/dist/components/project-session.d.ts +6 -3
- package/dist/components/project-session.js +14 -8
- package/dist/components/project-session.js.map +1 -1
- package/dist/components/project-switcher.js.map +1 -1
- package/dist/components/rail-view-source.js +2 -1
- package/dist/components/rail-view-source.js.map +1 -1
- package/dist/components/section-nav.js +1 -1
- package/dist/components/section-nav.js.map +1 -1
- package/dist/lib/ai/apply-content.js +56 -116
- package/dist/lib/ai/apply-content.js.map +1 -1
- package/dist/lib/ai/chat-context.d.ts +8 -0
- package/dist/lib/ai/chat-context.js +37 -6
- package/dist/lib/ai/chat-context.js.map +1 -1
- package/dist/lib/ai/chat-state.d.ts +14 -1
- package/dist/lib/ai/chat-state.js.map +1 -1
- package/dist/lib/ai/constraints.d.ts +20 -5
- package/dist/lib/ai/constraints.js +125 -33
- package/dist/lib/ai/constraints.js.map +1 -1
- package/dist/lib/ai/fake-provider.d.ts +16 -3
- package/dist/lib/ai/fake-provider.js +22 -6
- package/dist/lib/ai/fake-provider.js.map +1 -1
- package/dist/lib/ai/index.d.ts +2 -0
- package/dist/lib/ai/index.js +2 -0
- package/dist/lib/ai/index.js.map +1 -1
- package/dist/lib/ai/leash.d.ts +34 -0
- package/dist/lib/ai/leash.js +28 -0
- package/dist/lib/ai/leash.js.map +1 -0
- package/dist/lib/ai/provenance.d.ts +1 -1
- package/dist/lib/ai/provenance.js.map +1 -1
- package/dist/lib/ai/rich-text-input.d.ts +8 -8
- package/dist/lib/ai/rich-text-input.js +2 -1
- package/dist/lib/ai/rich-text-input.js.map +1 -1
- package/dist/lib/ai/rich-text-translate.d.ts +27 -4
- package/dist/lib/ai/rich-text-translate.js +28 -10
- package/dist/lib/ai/rich-text-translate.js.map +1 -1
- package/dist/lib/ai/shape.d.ts +13 -13
- package/dist/lib/ai/shape.js +69 -93
- package/dist/lib/ai/shape.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +36 -41
- package/dist/lib/ai/tools.js +85 -87
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/ai/translate-document.d.ts +39 -5
- package/dist/lib/ai/translate-document.js +160 -51
- package/dist/lib/ai/translate-document.js.map +1 -1
- package/dist/lib/ai/types.d.ts +34 -23
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.d.ts +21 -12
- package/dist/lib/ai/update-fields.js +118 -177
- package/dist/lib/ai/update-fields.js.map +1 -1
- package/dist/lib/ai/writer-form.d.ts +116 -0
- package/dist/lib/ai/writer-form.js +429 -0
- package/dist/lib/ai/writer-form.js.map +1 -0
- package/dist/lib/ai-current-document.d.ts +11 -11
- package/dist/lib/ai-current-document.js +4 -4
- package/dist/lib/ai-current-document.js.map +1 -1
- package/dist/lib/assets/upload.d.ts +31 -2
- package/dist/lib/assets/upload.js +61 -6
- package/dist/lib/assets/upload.js.map +1 -1
- package/dist/lib/clipboard/os-clipboard.d.ts +10 -2
- package/dist/lib/clipboard/os-clipboard.js +8 -4
- package/dist/lib/clipboard/os-clipboard.js.map +1 -1
- package/dist/lib/clipboard/paste-target.d.ts +18 -9
- package/dist/lib/clipboard/paste-target.js +8 -7
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/cms/field-patch.d.ts +15 -0
- package/dist/lib/cms/field-patch.js +25 -0
- package/dist/lib/cms/field-patch.js.map +1 -0
- package/dist/lib/cms/published-documents.d.ts +1 -1
- package/dist/lib/cms/published-documents.js +1 -1
- package/dist/lib/cms/published-documents.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +3 -3
- package/dist/lib/collection-view/__fixtures__/document-writes.d.ts +6 -0
- package/dist/lib/collection-view/__fixtures__/document-writes.js +23 -0
- package/dist/lib/collection-view/__fixtures__/document-writes.js.map +1 -0
- package/dist/lib/collection-view/collection-view-source.d.ts +69 -6
- package/dist/lib/collection-view/collection-view-source.js.map +1 -1
- package/dist/lib/data/collection-content.d.ts +4 -13
- package/dist/lib/data/collection-content.js +11 -5
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/dnd/drop-rules.d.ts +25 -31
- package/dist/lib/dnd/drop-rules.js +53 -59
- package/dist/lib/dnd/drop-rules.js.map +1 -1
- package/dist/lib/engine-version.d.ts +1 -1
- package/dist/lib/engine-version.js +1 -1
- package/dist/lib/engine-version.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +72 -15
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +61 -12
- package/dist/lib/i18n/messages.en.js +76 -16
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/__fixtures__/write-document.d.ts +30 -0
- package/dist/lib/operations/__fixtures__/write-document.js +16 -0
- package/dist/lib/operations/__fixtures__/write-document.js.map +1 -0
- package/dist/lib/operations/cms-persistence.d.ts +124 -31
- package/dist/lib/operations/cms-persistence.js +447 -68
- package/dist/lib/operations/cms-persistence.js.map +1 -1
- package/dist/lib/operations/config-manifest-lookup.d.ts +35 -0
- package/dist/lib/operations/config-manifest-lookup.js +56 -0
- package/dist/lib/operations/config-manifest-lookup.js.map +1 -0
- package/dist/lib/operations/project-runtime-config.d.ts +39 -0
- package/dist/lib/operations/project-runtime-config.js +54 -0
- package/dist/lib/operations/project-runtime-config.js.map +1 -0
- package/dist/lib/operations/publish-with-revision.d.ts +64 -0
- package/dist/lib/operations/publish-with-revision.js +66 -0
- package/dist/lib/operations/publish-with-revision.js.map +1 -0
- package/dist/lib/operations/runtime-validator.d.ts +32 -0
- package/dist/lib/operations/runtime-validator.js +18 -0
- package/dist/lib/operations/runtime-validator.js.map +1 -0
- package/dist/lib/operations/session-hydration.d.ts +31 -14
- package/dist/lib/operations/session-hydration.js +12 -16
- package/dist/lib/operations/session-hydration.js.map +1 -1
- package/dist/lib/operations/studio-operations.d.ts +59 -2
- package/dist/lib/operations/studio-operations.js +76 -14
- package/dist/lib/operations/studio-operations.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.d.ts +13 -5
- package/dist/lib/persistence/convex-adapter.js +189 -74
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persistence/convex-client.d.ts +65 -5
- package/dist/lib/persistence/convex-client.js +106 -3
- package/dist/lib/persistence/convex-client.js.map +1 -1
- package/dist/lib/persistence/index.js +17 -18
- package/dist/lib/persistence/index.js.map +1 -1
- package/dist/lib/persona/labels.js +4 -1
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/query/operations-query.d.ts +1 -1
- package/dist/lib/query/operations-query.js +1 -1
- package/dist/lib/query/operations-query.js.map +1 -1
- package/dist/lib/references/impact.d.ts +21 -15
- package/dist/lib/references/impact.js +13 -26
- package/dist/lib/references/impact.js.map +1 -1
- package/dist/lib/references/references-store.d.ts +9 -0
- package/dist/lib/references/references-store.js +24 -4
- package/dist/lib/references/references-store.js.map +1 -1
- package/dist/lib/routes/route-issues.d.ts +6 -6
- package/dist/lib/routes/route-issues.js +26 -80
- package/dist/lib/routes/route-issues.js.map +1 -1
- package/dist/lib/workspace/config-issues.js +2 -1
- package/dist/lib/workspace/config-issues.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +31 -1
- package/dist/lib/workspace/index.js +64 -17
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/lib/workspace/switcher-source.d.ts +3 -3
- package/dist/lib/workspace/switcher-source.js.map +1 -1
- package/dist/lib/workspace/workspace-source.d.ts +2 -2
- package/dist/lib/workspace/workspace-source.js +2 -1
- package/dist/lib/workspace/workspace-source.js.map +1 -1
- package/package.json +3 -3
- package/dist/components/cms/cms-composition-validators.d.ts +0 -16
- package/dist/components/cms/cms-composition-validators.js +0 -133
- package/dist/components/cms/cms-composition-validators.js.map +0 -1
- package/dist/components/cms/publish-clean.d.ts +0 -28
- package/dist/components/cms/publish-clean.js +0 -38
- package/dist/components/cms/publish-clean.js.map +0 -1
- package/dist/components/cms/use-save-mode.d.ts +0 -35
- package/dist/components/cms/use-save-mode.js +0 -54
- package/dist/components/cms/use-save-mode.js.map +0 -1
- package/dist/lib/assets/asset-placements.d.ts +0 -28
- package/dist/lib/assets/asset-placements.js +0 -83
- package/dist/lib/assets/asset-placements.js.map +0 -1
- package/dist/lib/cms/document-gate.d.ts +0 -47
- package/dist/lib/cms/document-gate.js +0 -61
- package/dist/lib/cms/document-gate.js.map +0 -1
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { applyDocumentPatches, fieldPath, isComponentNodeValue, locateNode, isSequenceValue, nodePath, nodePropPath, semanticValueEqual, } from '@elytracms/core/cms-core';
|
|
2
|
+
function index(content) {
|
|
3
|
+
const byId = new Map();
|
|
4
|
+
const order = [];
|
|
5
|
+
const typography = [];
|
|
6
|
+
let duplicate = false;
|
|
7
|
+
const visit = (list, parent, slot) => {
|
|
8
|
+
let previous;
|
|
9
|
+
list.forEach((entry, at) => {
|
|
10
|
+
if (!isComponentNodeValue(entry)) {
|
|
11
|
+
if (parent === null) {
|
|
12
|
+
previous = { typography: typography.length };
|
|
13
|
+
typography.push(entry);
|
|
14
|
+
}
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (byId.has(entry.id))
|
|
18
|
+
duplicate = true;
|
|
19
|
+
const placed = { node: entry, parent, slot, index: at, previous };
|
|
20
|
+
byId.set(entry.id, placed);
|
|
21
|
+
order.push(placed);
|
|
22
|
+
previous = { node: entry.id };
|
|
23
|
+
for (const [name, children] of Object.entries(entry.slots ?? {})) {
|
|
24
|
+
if (Array.isArray(children))
|
|
25
|
+
visit(children, entry.id, name);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
visit(content, null, undefined);
|
|
30
|
+
return { byId, order, typography, duplicate };
|
|
31
|
+
}
|
|
32
|
+
/** Longest common subsequence of two id lists (the siblings that kept their relative order). */
|
|
33
|
+
function lcs(a, b) {
|
|
34
|
+
const rows = a.length + 1;
|
|
35
|
+
const cols = b.length + 1;
|
|
36
|
+
const table = new Array(rows * cols).fill(0);
|
|
37
|
+
for (let i = a.length - 1; i >= 0; i -= 1) {
|
|
38
|
+
for (let j = b.length - 1; j >= 0; j -= 1) {
|
|
39
|
+
table[i * cols + j] =
|
|
40
|
+
a[i] === b[j]
|
|
41
|
+
? table[(i + 1) * cols + j + 1] + 1
|
|
42
|
+
: Math.max(table[(i + 1) * cols + j], table[i * cols + j + 1]);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const kept = new Set();
|
|
46
|
+
let i = 0;
|
|
47
|
+
let j = 0;
|
|
48
|
+
while (i < a.length && j < b.length) {
|
|
49
|
+
if (a[i] === b[j]) {
|
|
50
|
+
kept.add(a[i]);
|
|
51
|
+
i += 1;
|
|
52
|
+
j += 1;
|
|
53
|
+
}
|
|
54
|
+
else if (table[(i + 1) * cols + j] >= table[i * cols + j + 1])
|
|
55
|
+
i += 1;
|
|
56
|
+
else
|
|
57
|
+
j += 1;
|
|
58
|
+
}
|
|
59
|
+
return kept;
|
|
60
|
+
}
|
|
61
|
+
/** The keys a node may change without a whole-node rewrite. */
|
|
62
|
+
const STRUCTURAL_KEYS = new Set(['id', 'props', 'slots']);
|
|
63
|
+
/** A static prop (`{ value }`) — the only prop shape a `node.prop` patch carries. */
|
|
64
|
+
function staticValue(prop) {
|
|
65
|
+
if (typeof prop !== 'object' || prop === null || Array.isArray(prop))
|
|
66
|
+
return { ok: false };
|
|
67
|
+
const keys = Object.keys(prop);
|
|
68
|
+
if (keys.length !== 1 || keys[0] !== 'value')
|
|
69
|
+
return { ok: false };
|
|
70
|
+
return { ok: true, value: prop.value };
|
|
71
|
+
}
|
|
72
|
+
/** A new node's subtree without the descendants that already existed (they are moved in). */
|
|
73
|
+
function withoutExisting(node, existed) {
|
|
74
|
+
if (!node.slots)
|
|
75
|
+
return node;
|
|
76
|
+
const slots = {};
|
|
77
|
+
for (const [name, children] of Object.entries(node.slots)) {
|
|
78
|
+
if (!Array.isArray(children))
|
|
79
|
+
continue;
|
|
80
|
+
slots[name] = children
|
|
81
|
+
.filter((child) => !(isComponentNodeValue(child) && existed.has(child.id)))
|
|
82
|
+
.map((child) => (isComponentNodeValue(child) ? withoutExisting(child, existed) : child));
|
|
83
|
+
}
|
|
84
|
+
return { ...node, slots };
|
|
85
|
+
}
|
|
86
|
+
class Coarser extends Error {
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The patches that turn the composition field value `before` into `after` (both the
|
|
90
|
+
* stored v2 sequence, `undefined` when absent), addressed at `field` (+ `locale` for a
|
|
91
|
+
* localized field). See the module doc.
|
|
92
|
+
*/
|
|
93
|
+
export function compositionPatches(before, after, field, locale) {
|
|
94
|
+
const whole = fieldPath(field, locale);
|
|
95
|
+
if (semanticValueEqual(before, after))
|
|
96
|
+
return [];
|
|
97
|
+
if (after === undefined)
|
|
98
|
+
return [{ op: 'unset', path: whole }];
|
|
99
|
+
const beforeSequence = before === undefined ? { version: 2, content: [] } : before;
|
|
100
|
+
if (!isSequenceValue(beforeSequence) || !isSequenceValue(after))
|
|
101
|
+
return [{ op: 'set', path: whole, value: after }];
|
|
102
|
+
try {
|
|
103
|
+
return nodePatches(beforeSequence.content, after.content, field, locale);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (!(error instanceof Coarser))
|
|
107
|
+
throw error;
|
|
108
|
+
return [{ op: 'set', path: whole, value: after }];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function nodePatches(beforeContent, afterContent, field, locale) {
|
|
112
|
+
const was = index(beforeContent);
|
|
113
|
+
const now = index(afterContent);
|
|
114
|
+
if (was.duplicate || now.duplicate)
|
|
115
|
+
throw new Coarser();
|
|
116
|
+
// The canvas never edits typography: a typography change is not a node change.
|
|
117
|
+
if (!semanticValueEqual(was.typography, now.typography))
|
|
118
|
+
throw new Coarser();
|
|
119
|
+
// The structural patches apply in sequence; each position is computed against the
|
|
120
|
+
// value AS IT WILL BE when that patch applies (a simulation with THE merge), so the
|
|
121
|
+
// index is exact for an unchanged store — and anchored AFTER the previous component
|
|
122
|
+
// sibling, which keeps it right when a concurrent edit shifted the sequence.
|
|
123
|
+
let working = { collection: '_', id: '_', values: { [field]: { version: 2, content: [...beforeContent] } } };
|
|
124
|
+
const simulate = (patch) => {
|
|
125
|
+
const result = applyDocumentPatches({ document: working, patches: [patch], since: [] });
|
|
126
|
+
if (result.refused.length > 0)
|
|
127
|
+
throw new Coarser();
|
|
128
|
+
working = result.document;
|
|
129
|
+
};
|
|
130
|
+
const currentArray = (placed) => {
|
|
131
|
+
const value = working.values[field];
|
|
132
|
+
if (!isSequenceValue(value))
|
|
133
|
+
return [];
|
|
134
|
+
if (placed.parent === null)
|
|
135
|
+
return value.content;
|
|
136
|
+
const owner = locateNode(value, placed.parent);
|
|
137
|
+
const slot = owner?.node.slots?.[placed.slot ?? ''];
|
|
138
|
+
return Array.isArray(slot) ? slot : [];
|
|
139
|
+
};
|
|
140
|
+
const position = (placed, moving) => {
|
|
141
|
+
const parent = placed.parent === null ? fieldPath(field, locale) : nodePath(field, locale, placed.parent);
|
|
142
|
+
// A move detaches the node before it is placed: positions are read without it.
|
|
143
|
+
const list = currentArray(placed).filter((entry) => !(isComponentNodeValue(entry) && entry.id === moving));
|
|
144
|
+
let index = 0;
|
|
145
|
+
let anchor;
|
|
146
|
+
const previous = placed.previous;
|
|
147
|
+
if (previous && 'node' in previous) {
|
|
148
|
+
const at = list.findIndex((entry) => isComponentNodeValue(entry) && entry.id === previous.node);
|
|
149
|
+
index = at + 1;
|
|
150
|
+
anchor = previous.node;
|
|
151
|
+
}
|
|
152
|
+
else if (previous && 'typography' in previous) {
|
|
153
|
+
let seen = -1;
|
|
154
|
+
const at = list.findIndex((entry) => !isComponentNodeValue(entry) && ++seen === previous.typography);
|
|
155
|
+
index = at + 1;
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
parent,
|
|
159
|
+
...(placed.slot !== undefined ? { slot: placed.slot } : {}),
|
|
160
|
+
index: Math.max(0, index),
|
|
161
|
+
...(anchor !== undefined ? { after: anchor } : {}),
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
// Siblings that kept their relative order, per (unchanged) parent array. In the
|
|
165
|
+
// top-level sequence the typography items are fixed separators (no verb moves them),
|
|
166
|
+
// so a component also counts as moved when the number of typography items before it
|
|
167
|
+
// changed — the canvas re-anchors typography by component count on write-back
|
|
168
|
+
// (`mergeBlockRoots`), and the patches must reproduce exactly that value.
|
|
169
|
+
const keptInOrder = new Set();
|
|
170
|
+
const segmentOf = (indexed, placed) => {
|
|
171
|
+
if (placed.parent !== null)
|
|
172
|
+
return 0;
|
|
173
|
+
const list = indexed === was ? beforeContent : afterContent;
|
|
174
|
+
return list.slice(0, placed.index).filter((entry) => !isComponentNodeValue(entry)).length;
|
|
175
|
+
};
|
|
176
|
+
const siblingsOf = (indexed) => {
|
|
177
|
+
const groups = new Map();
|
|
178
|
+
for (const placed of indexed.order) {
|
|
179
|
+
const key = `${placed.parent ?? ''}\u0001${placed.slot ?? ''}`;
|
|
180
|
+
const list = groups.get(key) ?? [];
|
|
181
|
+
list.push({ id: placed.node.id, segment: segmentOf(indexed, placed) });
|
|
182
|
+
groups.set(key, list);
|
|
183
|
+
}
|
|
184
|
+
return groups;
|
|
185
|
+
};
|
|
186
|
+
const wasGroups = siblingsOf(was);
|
|
187
|
+
for (const [key, entries] of siblingsOf(now)) {
|
|
188
|
+
const nowSegment = new Map(entries.map((entry) => [entry.id, entry.segment]));
|
|
189
|
+
const previous = (wasGroups.get(key) ?? [])
|
|
190
|
+
.filter((entry) => nowSegment.get(entry.id) === entry.segment)
|
|
191
|
+
.map((entry) => entry.id);
|
|
192
|
+
const current = entries.map((entry) => entry.id).filter((id) => previous.includes(id));
|
|
193
|
+
for (const id of lcs(previous, current))
|
|
194
|
+
keptInOrder.add(id);
|
|
195
|
+
}
|
|
196
|
+
const structure = [];
|
|
197
|
+
const props = [];
|
|
198
|
+
for (const placed of now.order) {
|
|
199
|
+
const id = placed.node.id;
|
|
200
|
+
const prior = was.byId.get(id);
|
|
201
|
+
if (!prior) {
|
|
202
|
+
const parentIsNew = placed.parent !== null && !was.byId.has(placed.parent);
|
|
203
|
+
if (parentIsNew)
|
|
204
|
+
continue; // carried by its new parent's insert
|
|
205
|
+
const insert = { op: 'insert', at: position(placed), node: withoutExisting(placed.node, was.byId) };
|
|
206
|
+
simulate(insert);
|
|
207
|
+
structure.push(insert);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const parentIsNew = placed.parent !== null && !was.byId.has(placed.parent);
|
|
211
|
+
const moved = parentIsNew || prior.parent !== placed.parent || prior.slot !== placed.slot || !keptInOrder.has(id);
|
|
212
|
+
if (moved) {
|
|
213
|
+
const move = { op: 'move', path: nodePath(field, locale, id), to: position(placed, id) };
|
|
214
|
+
simulate(move);
|
|
215
|
+
structure.push(move);
|
|
216
|
+
}
|
|
217
|
+
// Anything but props / slots changed → no node verb says it.
|
|
218
|
+
for (const key of new Set([...Object.keys(prior.node), ...Object.keys(placed.node)])) {
|
|
219
|
+
if (STRUCTURAL_KEYS.has(key))
|
|
220
|
+
continue;
|
|
221
|
+
const a = prior.node[key];
|
|
222
|
+
const b = placed.node[key];
|
|
223
|
+
if (!semanticValueEqual(a, b)) {
|
|
224
|
+
throw new Coarser();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const priorProps = prior.node.props ?? {};
|
|
228
|
+
const nextProps = placed.node.props ?? {};
|
|
229
|
+
for (const name of new Set([...Object.keys(priorProps), ...Object.keys(nextProps)])) {
|
|
230
|
+
const a = priorProps[name];
|
|
231
|
+
const b = nextProps[name];
|
|
232
|
+
if (semanticValueEqual(a, b))
|
|
233
|
+
continue;
|
|
234
|
+
const path = nodePropPath(field, locale, id, name);
|
|
235
|
+
if (b === undefined) {
|
|
236
|
+
props.push({ op: 'unset', path });
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const value = staticValue(b);
|
|
240
|
+
if (!value.ok)
|
|
241
|
+
throw new Coarser();
|
|
242
|
+
props.push({ op: 'set', path, value: value.value });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// Removes LAST, top-most only: a child moved out of a removed container has left it.
|
|
246
|
+
const removes = [];
|
|
247
|
+
for (const placed of was.order) {
|
|
248
|
+
if (now.byId.has(placed.node.id))
|
|
249
|
+
continue;
|
|
250
|
+
if (placed.parent !== null && !now.byId.has(placed.parent) && was.byId.has(placed.parent))
|
|
251
|
+
continue;
|
|
252
|
+
removes.push({ op: 'remove', path: nodePath(field, locale, placed.node.id) });
|
|
253
|
+
}
|
|
254
|
+
return [...structure, ...removes, ...props];
|
|
255
|
+
}
|
|
256
|
+
//# sourceMappingURL=composition-patches.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"composition-patches.js","sourceRoot":"","sources":["../../../src/components/builder/composition-patches.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n applyDocumentPatches,\n fieldPath,\n isComponentNodeValue,\n locateNode,\n isSequenceValue,\n nodePath,\n nodePropPath,\n semanticValueEqual,\n} from '@elytracms/core/cms-core'\nimport type { CmsDocument, DocumentPatch, SequencePosition, ValuePath } from '@elytracms/core/cms-core'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\n\n/**\n * EC-595 A2 — the CANVAS WRITER's patches: a structural delta of one composition field,\n * keyed by component NODE ID, computed in the canvas write path from the field value\n * before and after one canvas action (insert / remove / duplicate / reorder / move /\n * replace / wrap / setProp / removeProp — and undo / redo, which are just another\n * before → after). It is \"at the source\" (the zone brief): the canvas knows its field\n * and its node ids; nothing diffs a document in the persister.\n *\n * The patches use the value-position grammar's sequence verbs (EC-595 binding point 3):\n *\n * - `insert` — a node new to the field, at its parent sequence (the field's top-level\n * sequence, or a component's named slot) + index, anchored AFTER its previous\n * component sibling; the inserted subtree carries its new descendants and leaves out\n * descendants that already existed (those are `move`d in);\n * - `move` — an existing node whose parent / slot changed, or whose order among its\n * siblings did (the nodes outside the longest common subsequence of the sibling ids);\n * - `remove` — a node gone from the field (only the top-most of a removed subtree);\n * - `set` / `unset` at `node.prop` — a static prop whose value changed.\n *\n * So two sessions inserting into one page both land (two `insert`s into one parent\n * never conflict); a prop edit on node X and a move of node Y in another session both\n * apply; only a change to the SAME node position conflicts (the merge's prefix rule).\n *\n * When a change cannot be said with node verbs — the typography items (which the canvas\n * never edits) differ, a node changes something other than its props / slots, a prop is\n * not a static value, duplicate ids — the delta is the coarser legal path: the whole\n * field (`set` / `unset`), and conflict detection is correspondingly coarser, never\n * wrong (value-path.ts: \"writers may always emit a coarser path\").\n */\n\ninterface Placed {\n readonly node: ComponentNode\n /** `null` = the field's top-level sequence. */\n readonly parent: string | null\n readonly slot: string | undefined\n /** The index in the parent array (the full top-level `content`, or the slot list). */\n readonly index: number\n /** The entry immediately before it in the parent array: a component id, or the ordinal of a typography item. */\n readonly previous: { readonly node: string } | { readonly typography: number } | undefined\n}\n\ninterface Indexed {\n readonly byId: Map<string, Placed>\n /** Pre-order. */\n readonly order: readonly Placed[]\n readonly typography: readonly unknown[]\n readonly duplicate: boolean\n}\n\nfunction index(content: readonly unknown[]): Indexed {\n const byId = new Map<string, Placed>()\n const order: Placed[] = []\n const typography: unknown[] = []\n let duplicate = false\n const visit = (list: readonly unknown[], parent: string | null, slot: string | undefined) => {\n let previous: Placed['previous']\n list.forEach((entry, at) => {\n if (!isComponentNodeValue(entry)) {\n if (parent === null) {\n previous = { typography: typography.length }\n typography.push(entry)\n }\n return\n }\n if (byId.has(entry.id)) duplicate = true\n const placed: Placed = { node: entry, parent, slot, index: at, previous }\n byId.set(entry.id, placed)\n order.push(placed)\n previous = { node: entry.id }\n for (const [name, children] of Object.entries(entry.slots ?? {})) {\n if (Array.isArray(children)) visit(children, entry.id, name)\n }\n })\n }\n visit(content, null, undefined)\n return { byId, order, typography, duplicate }\n}\n\n/** Longest common subsequence of two id lists (the siblings that kept their relative order). */\nfunction lcs(a: readonly string[], b: readonly string[]): Set<string> {\n const rows = a.length + 1\n const cols = b.length + 1\n const table: number[] = new Array(rows * cols).fill(0)\n for (let i = a.length - 1; i >= 0; i -= 1) {\n for (let j = b.length - 1; j >= 0; j -= 1) {\n table[i * cols + j] =\n a[i] === b[j]\n ? table[(i + 1) * cols + j + 1]! + 1\n : Math.max(table[(i + 1) * cols + j]!, table[i * cols + j + 1]!)\n }\n }\n const kept = new Set<string>()\n let i = 0\n let j = 0\n while (i < a.length && j < b.length) {\n if (a[i] === b[j]) {\n kept.add(a[i]!)\n i += 1\n j += 1\n } else if (table[(i + 1) * cols + j]! >= table[i * cols + j + 1]!) i += 1\n else j += 1\n }\n return kept\n}\n\n/** The keys a node may change without a whole-node rewrite. */\nconst STRUCTURAL_KEYS = new Set(['id', 'props', 'slots'])\n\n/** A static prop (`{ value }`) — the only prop shape a `node.prop` patch carries. */\nfunction staticValue(prop: unknown): { ok: true; value: unknown } | { ok: false } {\n if (typeof prop !== 'object' || prop === null || Array.isArray(prop)) return { ok: false }\n const keys = Object.keys(prop)\n if (keys.length !== 1 || keys[0] !== 'value') return { ok: false }\n return { ok: true, value: (prop as { value: unknown }).value }\n}\n\n/** A new node's subtree without the descendants that already existed (they are moved in). */\nfunction withoutExisting(node: ComponentNode, existed: ReadonlyMap<string, Placed>): ComponentNode {\n if (!node.slots) return node\n const slots: Record<string, ComponentNode[]> = {}\n for (const [name, children] of Object.entries(node.slots)) {\n if (!Array.isArray(children)) continue\n slots[name] = children\n .filter((child) => !(isComponentNodeValue(child) && existed.has(child.id)))\n .map((child) => (isComponentNodeValue(child) ? withoutExisting(child, existed) : child))\n }\n return { ...node, slots }\n}\n\nclass Coarser extends Error {}\n\n/**\n * The patches that turn the composition field value `before` into `after` (both the\n * stored v2 sequence, `undefined` when absent), addressed at `field` (+ `locale` for a\n * localized field). See the module doc.\n */\nexport function compositionPatches(\n before: unknown,\n after: unknown,\n field: string,\n locale: string | undefined,\n): DocumentPatch[] {\n const whole = fieldPath(field, locale)\n if (semanticValueEqual(before, after)) return []\n if (after === undefined) return [{ op: 'unset', path: whole }]\n const beforeSequence = before === undefined ? { version: 2 as const, content: [] } : before\n if (!isSequenceValue(beforeSequence) || !isSequenceValue(after)) return [{ op: 'set', path: whole, value: after }]\n try {\n return nodePatches(beforeSequence.content, after.content, field, locale)\n } catch (error) {\n if (!(error instanceof Coarser)) throw error\n return [{ op: 'set', path: whole, value: after }]\n }\n}\n\nfunction nodePatches(\n beforeContent: readonly unknown[],\n afterContent: readonly unknown[],\n field: string,\n locale: string | undefined,\n): DocumentPatch[] {\n const was = index(beforeContent)\n const now = index(afterContent)\n if (was.duplicate || now.duplicate) throw new Coarser()\n // The canvas never edits typography: a typography change is not a node change.\n if (!semanticValueEqual(was.typography, now.typography)) throw new Coarser()\n\n // The structural patches apply in sequence; each position is computed against the\n // value AS IT WILL BE when that patch applies (a simulation with THE merge), so the\n // index is exact for an unchanged store — and anchored AFTER the previous component\n // sibling, which keeps it right when a concurrent edit shifted the sequence.\n let working: CmsDocument = { collection: '_', id: '_', values: { [field]: { version: 2, content: [...beforeContent] } } }\n const simulate = (patch: DocumentPatch) => {\n const result = applyDocumentPatches({ document: working, patches: [patch], since: [] })\n if (result.refused.length > 0) throw new Coarser()\n working = result.document\n }\n const currentArray = (placed: Placed): readonly unknown[] => {\n const value = working.values[field]\n if (!isSequenceValue(value)) return []\n if (placed.parent === null) return value.content\n const owner = locateNode(value, placed.parent)\n const slot = (owner?.node.slots as Record<string, unknown> | undefined)?.[placed.slot ?? '']\n return Array.isArray(slot) ? slot : []\n }\n const position = (placed: Placed, moving?: string): SequencePosition => {\n const parent: ValuePath = placed.parent === null ? fieldPath(field, locale) : nodePath(field, locale, placed.parent)\n // A move detaches the node before it is placed: positions are read without it.\n const list = currentArray(placed).filter((entry) => !(isComponentNodeValue(entry) && entry.id === moving))\n let index = 0\n let anchor: string | undefined\n const previous = placed.previous\n if (previous && 'node' in previous) {\n const at = list.findIndex((entry) => isComponentNodeValue(entry) && entry.id === previous.node)\n index = at + 1\n anchor = previous.node\n } else if (previous && 'typography' in previous) {\n let seen = -1\n const at = list.findIndex((entry) => !isComponentNodeValue(entry) && ++seen === previous.typography)\n index = at + 1\n }\n return {\n parent,\n ...(placed.slot !== undefined ? { slot: placed.slot } : {}),\n index: Math.max(0, index),\n ...(anchor !== undefined ? { after: anchor } : {}),\n }\n }\n\n // Siblings that kept their relative order, per (unchanged) parent array. In the\n // top-level sequence the typography items are fixed separators (no verb moves them),\n // so a component also counts as moved when the number of typography items before it\n // changed — the canvas re-anchors typography by component count on write-back\n // (`mergeBlockRoots`), and the patches must reproduce exactly that value.\n const keptInOrder = new Set<string>()\n const segmentOf = (indexed: Indexed, placed: Placed) => {\n if (placed.parent !== null) return 0\n const list = indexed === was ? beforeContent : afterContent\n return list.slice(0, placed.index).filter((entry) => !isComponentNodeValue(entry)).length\n }\n const siblingsOf = (indexed: Indexed) => {\n const groups = new Map<string, { id: string; segment: number }[]>()\n for (const placed of indexed.order) {\n const key = `${placed.parent ?? ''}\\u0001${placed.slot ?? ''}`\n const list = groups.get(key) ?? []\n list.push({ id: placed.node.id, segment: segmentOf(indexed, placed) })\n groups.set(key, list)\n }\n return groups\n }\n const wasGroups = siblingsOf(was)\n for (const [key, entries] of siblingsOf(now)) {\n const nowSegment = new Map(entries.map((entry) => [entry.id, entry.segment]))\n const previous = (wasGroups.get(key) ?? [])\n .filter((entry) => nowSegment.get(entry.id) === entry.segment)\n .map((entry) => entry.id)\n const current = entries.map((entry) => entry.id).filter((id) => previous.includes(id))\n for (const id of lcs(previous, current)) keptInOrder.add(id)\n }\n\n const structure: DocumentPatch[] = []\n const props: DocumentPatch[] = []\n for (const placed of now.order) {\n const id = placed.node.id\n const prior = was.byId.get(id)\n if (!prior) {\n const parentIsNew = placed.parent !== null && !was.byId.has(placed.parent)\n if (parentIsNew) continue // carried by its new parent's insert\n const insert: DocumentPatch = { op: 'insert', at: position(placed), node: withoutExisting(placed.node, was.byId) }\n simulate(insert)\n structure.push(insert)\n continue\n }\n const parentIsNew = placed.parent !== null && !was.byId.has(placed.parent)\n const moved =\n parentIsNew || prior.parent !== placed.parent || prior.slot !== placed.slot || !keptInOrder.has(id)\n if (moved) {\n const move: DocumentPatch = { op: 'move', path: nodePath(field, locale, id), to: position(placed, id) }\n simulate(move)\n structure.push(move)\n }\n // Anything but props / slots changed → no node verb says it.\n for (const key of new Set([...Object.keys(prior.node), ...Object.keys(placed.node)])) {\n if (STRUCTURAL_KEYS.has(key)) continue\n const a = (prior.node as unknown as Record<string, unknown>)[key]\n const b = (placed.node as unknown as Record<string, unknown>)[key]\n if (!semanticValueEqual(a, b)) {\n throw new Coarser()\n }\n }\n const priorProps = prior.node.props ?? {}\n const nextProps = placed.node.props ?? {}\n for (const name of new Set([...Object.keys(priorProps), ...Object.keys(nextProps)])) {\n const a = (priorProps as Record<string, unknown>)[name]\n const b = (nextProps as Record<string, unknown>)[name]\n if (semanticValueEqual(a, b)) continue\n const path = nodePropPath(field, locale, id, name)\n if (b === undefined) {\n props.push({ op: 'unset', path })\n continue\n }\n const value = staticValue(b)\n if (!value.ok) throw new Coarser()\n props.push({ op: 'set', path, value: value.value })\n }\n }\n\n // Removes LAST, top-most only: a child moved out of a removed container has left it.\n const removes: DocumentPatch[] = []\n for (const placed of was.order) {\n if (now.byId.has(placed.node.id)) continue\n if (placed.parent !== null && !now.byId.has(placed.parent) && was.byId.has(placed.parent)) continue\n removes.push({ op: 'remove', path: nodePath(field, locale, placed.node.id) })\n }\n return [...structure, ...removes, ...props]\n}\n"]}
|
|
@@ -23,6 +23,11 @@ export interface DeleteKeyListenerOptions {
|
|
|
23
23
|
getRootId: () => string | null;
|
|
24
24
|
/** Extra suppression (e.g. a read-only published preview). */
|
|
25
25
|
isDisabled?: () => boolean;
|
|
26
|
+
/**
|
|
27
|
+
* EC-549: remove the selected typography RUN, when one is selected (it takes
|
|
28
|
+
* precedence — a run and a node are never both selected); returns whether it did.
|
|
29
|
+
*/
|
|
30
|
+
removeSelectedRun?: () => boolean;
|
|
26
31
|
}
|
|
27
32
|
interface ListenerTarget {
|
|
28
33
|
addEventListener(type: string, listener: (e: Event) => void): void;
|
|
@@ -34,6 +34,10 @@ export function attachDeleteKeyListener(target, opts) {
|
|
|
34
34
|
return;
|
|
35
35
|
if (isEditingContext(event.target))
|
|
36
36
|
return;
|
|
37
|
+
if (opts.removeSelectedRun?.()) {
|
|
38
|
+
event.preventDefault();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
37
41
|
const selectedId = opts.getSelectedId();
|
|
38
42
|
if (!selectedId || selectedId === opts.getRootId())
|
|
39
43
|
return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"delete-key.js","sourceRoot":"","sources":["../../../src/components/builder/delete-key.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAE3D;;;;;;;;GAQG;AAEH;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAA0B;IACzD,OAAO,qBAAqB,CAAC,MAAM,CAAC,CAAA;AACtC,CAAC;
|
|
1
|
+
{"version":3,"file":"delete-key.js","sourceRoot":"","sources":["../../../src/components/builder/delete-key.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAE3D;;;;;;;;GAQG;AAEH;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAA0B;IACzD,OAAO,qBAAqB,CAAC,MAAM,CAAC,CAAA;AACtC,CAAC;AAuBD;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CACrC,MAAsB,EACtB,IAA8B;IAE9B,MAAM,SAAS,GAAG,CAAC,CAAQ,EAAQ,EAAE;QACnC,MAAM,KAAK,GAAG,CAAkB,CAAA;QAChC,IAAI,KAAK,CAAC,GAAG,KAAK,QAAQ,IAAI,KAAK,CAAC,GAAG,KAAK,WAAW;YAAE,OAAM;QAC/D,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,MAAM;YAAE,OAAM;QAC1D,IAAI,IAAI,CAAC,UAAU,EAAE,EAAE,IAAI,KAAK;YAAE,OAAM;QACxC,IAAI,gBAAgB,CAAC,KAAK,CAAC,MAAM,CAAC;YAAE,OAAM;QAC1C,IAAI,IAAI,CAAC,iBAAiB,EAAE,EAAE,EAAE,CAAC;YAC/B,KAAK,CAAC,cAAc,EAAE,CAAA;YACtB,OAAM;QACR,CAAC;QACD,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa,EAAE,CAAA;QACvC,IAAI,CAAC,UAAU,IAAI,UAAU,KAAK,IAAI,CAAC,SAAS,EAAE;YAAE,OAAM;QAC1D,KAAK,CAAC,cAAc,EAAE,CAAA;QACtB,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;IACzB,CAAC,CAAA;IACD,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;IAC7C,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;AAC/D,CAAC","sourcesContent":["import { clipboardEventBlocked } from './clipboard-actions'\n\n/**\n * Delete/Backspace removes the selected node (EC-404, scope 3) — the gap the\n * findings batch surfaced: the studio had no keyboard remove at all except cut\n * (⌘X). This sits beside the clipboard listeners (`attachClipboardListeners`),\n * wired on the same two surfaces (the studio window for the page tree, the\n * iframe document for the canvas), and reuses the SAME editing-context guard so\n * pressing Backspace while typing in an input or an inline text/rich-text editor\n * edits the text — never deletes the block.\n */\n\n/**\n * True when a key event's target owns its own text-editing behavior — a form\n * field or any contenteditable (which includes the EC-158/EC-159 inline\n * editors). The single guard for the Delete/Backspace remove, kept identical to\n * the clipboard shortcuts' scope so the two can never disagree.\n */\nexport function isEditingContext(target: EventTarget | null): boolean {\n return clipboardEventBlocked(target)\n}\n\nexport interface DeleteKeyListenerOptions {\n /** The current selection — the remove subject. */\n getSelectedId: () => string | null\n /** Dispatch the remove (the existing `remove` tree action; one undo restores). */\n remove: (id: string) => void\n /** The id that must never be removed (the composition root). */\n getRootId: () => string | null\n /** Extra suppression (e.g. a read-only published preview). */\n isDisabled?: () => boolean\n /**\n * EC-549: remove the selected typography RUN, when one is selected (it takes\n * precedence — a run and a node are never both selected); returns whether it did.\n */\n removeSelectedRun?: () => boolean\n}\n\ninterface ListenerTarget {\n addEventListener(type: string, listener: (e: Event) => void): void\n removeEventListener(type: string, listener: (e: Event) => void): void\n}\n\n/**\n * Wire Delete/Backspace → remove the selected node. Inert when the event\n * targets an editing context, when nothing (or the root) is selected, or when\n * disabled. Modifier combinations are ignored so it never shadows a browser\n * shortcut (⌘Delete etc.).\n */\nexport function attachDeleteKeyListener(\n target: ListenerTarget,\n opts: DeleteKeyListenerOptions,\n): () => void {\n const onKeyDown = (e: Event): void => {\n const event = e as KeyboardEvent\n if (event.key !== 'Delete' && event.key !== 'Backspace') return\n if (event.metaKey || event.ctrlKey || event.altKey) return\n if (opts.isDisabled?.() ?? false) return\n if (isEditingContext(event.target)) return\n if (opts.removeSelectedRun?.()) {\n event.preventDefault()\n return\n }\n const selectedId = opts.getSelectedId()\n if (!selectedId || selectedId === opts.getRootId()) return\n event.preventDefault()\n opts.remove(selectedId)\n }\n target.addEventListener('keydown', onKeyDown)\n return () => target.removeEventListener('keydown', onKeyDown)\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ComponentNode } from '@elytracms/core/project-graph';
|
|
1
|
+
import type { ComponentNode, PlacementScope } from '@elytracms/core/project-graph';
|
|
2
2
|
import type { StudioRegistry } from '@elytracms/studio/lib/registry';
|
|
3
3
|
import type { InsertContext } from './insert-menu.js';
|
|
4
4
|
/**
|
|
@@ -9,22 +9,18 @@ import type { InsertContext } from './insert-menu.js';
|
|
|
9
9
|
* Canvas editor (`composition-field-editor.tsx`, EC-188/AD-12) can own it without
|
|
10
10
|
* importing the dead shell. No UI here — pure graph/registry resolution.
|
|
11
11
|
*/
|
|
12
|
+
/** What the resolution reads from the builder store: the tree and its placement scope (EC-584). */
|
|
13
|
+
export interface InsertScope {
|
|
14
|
+
present: ComponentNode;
|
|
15
|
+
placement: PlacementScope;
|
|
16
|
+
}
|
|
12
17
|
/**
|
|
13
|
-
* Resolve where an insert lands: into the node's child slot, else as a sibling.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* active page root — the top level of a composition field value — where it is
|
|
18
|
-
* intersected into `slotAllow` so forbidden components are never even offered,
|
|
19
|
-
* mirroring the top-level leash in `validateCompositionValue`. Deeper inserts keep
|
|
20
|
-
* following each component's own slot `allow`. Callers that omit `fieldAllow` get
|
|
21
|
-
* an unconstrained top level. EC-554: which placements the field governs is read
|
|
22
|
-
* through `fieldAllowAt` — the SAME reader drag-and-drop and paste judge with, so
|
|
23
|
-
* the offer and the placement can never disagree.
|
|
18
|
+
* Resolve where an insert lands: into the node's child slot, else as a sibling. The
|
|
19
|
+
* offered vocabulary is the landing position's (see {@link contextAt}) — the SAME
|
|
20
|
+
* reader the reducer, drag-and-drop and paste judge with, so the offer and the
|
|
21
|
+
* placement can never disagree.
|
|
24
22
|
*/
|
|
25
|
-
export declare function resolveInsertContext(builder:
|
|
26
|
-
present: ComponentNode;
|
|
27
|
-
}, nodeId: string, fieldAllow?: readonly string[], studio?: StudioRegistry): InsertContext | null;
|
|
23
|
+
export declare function resolveInsertContext(builder: InsertScope, nodeId: string, studio?: StudioRegistry): InsertContext | null;
|
|
28
24
|
/**
|
|
29
25
|
* An in-canvas insertion position relative to a reference node (EC-405) — the
|
|
30
26
|
* DnD position vocabulary (`lib/dnd/drop-rules`), narrowed to the three the
|
|
@@ -42,17 +38,15 @@ export type InsertPosition = 'before' | 'after' | 'into';
|
|
|
42
38
|
* {@link primaryInsertSlot} (the same slot a drop `into` picks; `null` when the
|
|
43
39
|
* node accepts no children), `before`/`after` use the reference's parent slot at
|
|
44
40
|
* `index`/`index + 1` — so this surface and drag-insert can never disagree about
|
|
45
|
-
* where a position lands or what it allows
|
|
46
|
-
*
|
|
41
|
+
* where a position lands or what it allows (EC-584: the vocabulary is `allowAt`'s,
|
|
42
|
+
* exactly as in `resolveInsertContext`).
|
|
47
43
|
*/
|
|
48
|
-
export declare function resolveInsertContextAt(builder:
|
|
49
|
-
present: ComponentNode;
|
|
50
|
-
}, nodeId: string, position: InsertPosition, fieldAllow?: readonly string[], studio?: StudioRegistry): InsertContext | null;
|
|
44
|
+
export declare function resolveInsertContextAt(builder: InsertScope, nodeId: string, position: InsertPosition, studio?: StudioRegistry): InsertContext | null;
|
|
51
45
|
/**
|
|
52
46
|
* Which affordance positions are actually insertable at `nodeId` (EC-405 scope 3):
|
|
53
47
|
* a position is offered only when it structurally exists AND at least one registry
|
|
54
|
-
* component may land there (the SAME
|
|
55
|
-
*
|
|
48
|
+
* component may land there (the SAME judge the tree/DnD, the blocks panel and the
|
|
49
|
+
* reducer apply — {@link hasInsertableComponent} over the resolved `allow`), so a
|
|
56
50
|
* blocked spot shows no dead button.
|
|
57
51
|
*
|
|
58
52
|
* An EMPTY, insertable container surfaces ONLY `into` — the meaningful action there
|
|
@@ -62,6 +56,4 @@ export declare function resolveInsertContextAt(builder: {
|
|
|
62
56
|
* child already carries its own edge affordances, so a filled container needs no
|
|
63
57
|
* center "+").
|
|
64
58
|
*/
|
|
65
|
-
export declare function insertablePositions(builder:
|
|
66
|
-
present: ComponentNode;
|
|
67
|
-
}, nodeId: string, fieldAllow?: readonly string[], studio?: StudioRegistry): InsertPosition[];
|
|
59
|
+
export declare function insertablePositions(builder: InsertScope, nodeId: string, studio?: StudioRegistry): InsertPosition[];
|
|
@@ -1,30 +1,29 @@
|
|
|
1
|
-
import { getNode, getParentInfo } from '@elytracms/core/project-graph';
|
|
1
|
+
import { allowAt, getNode, getParentInfo } from '@elytracms/core/project-graph';
|
|
2
2
|
import { getStudioRegistry } from '@elytracms/studio/lib/registry';
|
|
3
3
|
import { componentDisplayName } from '@elytracms/studio/lib/persona/labels';
|
|
4
|
-
import {
|
|
5
|
-
import { hasInsertableComponent
|
|
4
|
+
import { manifestRules, primaryInsertSlot } from '@elytracms/studio/lib/dnd/drop-rules';
|
|
5
|
+
import { hasInsertableComponent } from './blocks-model.js';
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* importing the dead shell. No UI here — pure graph/registry resolution.
|
|
7
|
+
* The offer at a target: its EFFECTIVE vocabulary, read through the core judge's own
|
|
8
|
+
* reader (`allowAt` — the composition field's at the composition root, keyed on node
|
|
9
|
+
* identity, mirroring the top-level leash in `validateCompositionValue`; each slot's
|
|
10
|
+
* own `allow` below), so the offer is exactly what the reducer admits (EC-584).
|
|
11
|
+
* EC-544: a closed set that admits nothing is the "No allowed components match" state.
|
|
13
12
|
*/
|
|
13
|
+
function contextAt(builder, target, destinationLabel) {
|
|
14
|
+
return {
|
|
15
|
+
target,
|
|
16
|
+
allow: allowAt(builder.present, target.parentId, target.slot, builder.placement),
|
|
17
|
+
destinationLabel,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
14
20
|
/**
|
|
15
|
-
* Resolve where an insert lands: into the node's child slot, else as a sibling.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* active page root — the top level of a composition field value — where it is
|
|
20
|
-
* intersected into `slotAllow` so forbidden components are never even offered,
|
|
21
|
-
* mirroring the top-level leash in `validateCompositionValue`. Deeper inserts keep
|
|
22
|
-
* following each component's own slot `allow`. Callers that omit `fieldAllow` get
|
|
23
|
-
* an unconstrained top level. EC-554: which placements the field governs is read
|
|
24
|
-
* through `fieldAllowAt` — the SAME reader drag-and-drop and paste judge with, so
|
|
25
|
-
* the offer and the placement can never disagree.
|
|
21
|
+
* Resolve where an insert lands: into the node's child slot, else as a sibling. The
|
|
22
|
+
* offered vocabulary is the landing position's (see {@link contextAt}) — the SAME
|
|
23
|
+
* reader the reducer, drag-and-drop and paste judge with, so the offer and the
|
|
24
|
+
* placement can never disagree.
|
|
26
25
|
*/
|
|
27
|
-
export function resolveInsertContext(builder, nodeId,
|
|
26
|
+
export function resolveInsertContext(builder, nodeId, studio = getStudioRegistry()) {
|
|
28
27
|
const root = builder.present;
|
|
29
28
|
const node = getNode(root, nodeId);
|
|
30
29
|
if (!node)
|
|
@@ -35,47 +34,24 @@ export function resolveInsertContext(builder, nodeId, fieldAllow, studio = getSt
|
|
|
35
34
|
// instead of silently degrading to a sibling insert.
|
|
36
35
|
const manifest = studio.registry.getManifest(node.componentId);
|
|
37
36
|
const childSlot = primaryInsertSlot(manifestRules(manifest));
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
destinationLabel: `${componentDisplayName(node.componentId, manifest)} · ${childSlot.name}`,
|
|
43
|
-
}
|
|
44
|
-
: resolveSiblingContext(studio, root, nodeId);
|
|
45
|
-
// The field vocabulary governs only the top level (a direct child of the page
|
|
46
|
-
// root). EC-544: an `allow` list is a closed set — absent = any, a list (even
|
|
47
|
-
// an empty one) = exactly those ids — the same reading the validator applies,
|
|
48
|
-
// so an empty intersection here is the "No allowed components match" state.
|
|
49
|
-
return withFieldLeash(root, ctx, fieldAllow);
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Layer the composition field's top-level vocabulary over the resolved slot
|
|
53
|
-
* `allow` (EC-188) — for a direct child of the root only, as decided by
|
|
54
|
-
* {@link fieldAllowAt} (EC-554: the one reader every placement path shares).
|
|
55
|
-
*/
|
|
56
|
-
function withFieldLeash(root, ctx, fieldAllow) {
|
|
57
|
-
const leash = fieldAllowAt(root, ctx.target.parentId, fieldAllow);
|
|
58
|
-
return leash ? { ...ctx, slotAllow: intersectAllow(ctx.slotAllow, leash) } : ctx;
|
|
37
|
+
if (childSlot) {
|
|
38
|
+
return contextAt(builder, { parentId: nodeId, slot: childSlot.name }, `${componentDisplayName(node.componentId, manifest)} · ${childSlot.name}`);
|
|
39
|
+
}
|
|
40
|
+
return resolveSiblingContext(studio, builder, nodeId);
|
|
59
41
|
}
|
|
60
42
|
/** A destination's component label — its manifest title (EC-591: never a stripped id prefix). */
|
|
61
43
|
function parentLabel(studio, componentId) {
|
|
62
44
|
return componentDisplayName(componentId, studio.registry.getManifest(componentId));
|
|
63
45
|
}
|
|
64
46
|
/** Sibling-or-root insert context for a node with no insertable child slot. */
|
|
65
|
-
function resolveSiblingContext(studio,
|
|
66
|
-
const info = getParentInfo(
|
|
47
|
+
function resolveSiblingContext(studio, builder, nodeId) {
|
|
48
|
+
const info = getParentInfo(builder.present, nodeId);
|
|
67
49
|
if (info) {
|
|
68
|
-
// EC-386: the parent slot may be a blocks-prop region —
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
const allow = pManifest?.slots.find((s) => s.name === info.slot)?.allow;
|
|
72
|
-
return {
|
|
73
|
-
target: { parentId: info.parent.id, slot: info.slot, index: info.index + 1 },
|
|
74
|
-
slotAllow: allow,
|
|
75
|
-
destinationLabel: `${parentLabel(studio, info.parent.componentId)} · ${info.slot}`,
|
|
76
|
-
};
|
|
50
|
+
// EC-386: the parent slot may be a blocks-prop region — its `allow` leash still
|
|
51
|
+
// filters sibling inserts (the core lookup merges blocks props into the slots).
|
|
52
|
+
return contextAt(builder, { parentId: info.parent.id, slot: info.slot, index: info.index + 1 }, `${parentLabel(studio, info.parent.componentId)} · ${info.slot}`);
|
|
77
53
|
}
|
|
78
|
-
return
|
|
54
|
+
return contextAt(builder, { parentId: nodeId, slot: 'children' }, 'root');
|
|
79
55
|
}
|
|
80
56
|
/**
|
|
81
57
|
* Resolve an `InsertContext` for an EXPLICIT position relative to `nodeId`
|
|
@@ -87,15 +63,14 @@ function resolveSiblingContext(studio, root, nodeId) {
|
|
|
87
63
|
* {@link primaryInsertSlot} (the same slot a drop `into` picks; `null` when the
|
|
88
64
|
* node accepts no children), `before`/`after` use the reference's parent slot at
|
|
89
65
|
* `index`/`index + 1` — so this surface and drag-insert can never disagree about
|
|
90
|
-
* where a position lands or what it allows
|
|
91
|
-
*
|
|
66
|
+
* where a position lands or what it allows (EC-584: the vocabulary is `allowAt`'s,
|
|
67
|
+
* exactly as in `resolveInsertContext`).
|
|
92
68
|
*/
|
|
93
|
-
export function resolveInsertContextAt(builder, nodeId, position,
|
|
69
|
+
export function resolveInsertContextAt(builder, nodeId, position, studio = getStudioRegistry()) {
|
|
94
70
|
const root = builder.present;
|
|
95
71
|
const node = getNode(root, nodeId);
|
|
96
72
|
if (!node)
|
|
97
73
|
return null;
|
|
98
|
-
let ctx;
|
|
99
74
|
if (position === 'into') {
|
|
100
75
|
// EC-386: `manifestRules` folds blocks-prop regions into the slot set, so
|
|
101
76
|
// an affordance/drop `into` a Section-style body lands in that region.
|
|
@@ -103,40 +78,18 @@ export function resolveInsertContextAt(builder, nodeId, position, fieldAllow, st
|
|
|
103
78
|
const slot = primaryInsertSlot(manifestRules(manifest));
|
|
104
79
|
if (!slot)
|
|
105
80
|
return null; // the node accepts no children — no `into` position
|
|
106
|
-
|
|
107
|
-
target: {
|
|
108
|
-
parentId: nodeId,
|
|
109
|
-
slot: slot.name,
|
|
110
|
-
index: (node.slots?.[slot.name] ?? []).length,
|
|
111
|
-
},
|
|
112
|
-
slotAllow: slot.allow,
|
|
113
|
-
destinationLabel: `${componentDisplayName(node.componentId, manifest)} · ${slot.name}`,
|
|
114
|
-
};
|
|
81
|
+
return contextAt(builder, { parentId: nodeId, slot: slot.name, index: (node.slots?.[slot.name] ?? []).length }, `${componentDisplayName(node.componentId, manifest)} · ${slot.name}`);
|
|
115
82
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// EC-386: merged vocabulary — a blocks-prop parent region keeps its leash.
|
|
121
|
-
const pManifest = manifestRules(studio.registry.getManifest(info.parent.componentId));
|
|
122
|
-
ctx = {
|
|
123
|
-
target: {
|
|
124
|
-
parentId: info.parent.id,
|
|
125
|
-
slot: info.slot,
|
|
126
|
-
index: info.index + (position === 'after' ? 1 : 0),
|
|
127
|
-
},
|
|
128
|
-
slotAllow: pManifest?.slots.find((s) => s.name === info.slot)?.allow,
|
|
129
|
-
destinationLabel: `${parentLabel(studio, info.parent.componentId)} · ${info.slot}`,
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
// EC-544: closed-set reading, same as `resolveInsertContext` above.
|
|
133
|
-
return withFieldLeash(root, ctx, fieldAllow);
|
|
83
|
+
const info = getParentInfo(root, nodeId);
|
|
84
|
+
if (!info)
|
|
85
|
+
return null; // the root has no siblings — no before/after position
|
|
86
|
+
return contextAt(builder, { parentId: info.parent.id, slot: info.slot, index: info.index + (position === 'after' ? 1 : 0) }, `${parentLabel(studio, info.parent.componentId)} · ${info.slot}`);
|
|
134
87
|
}
|
|
135
88
|
/**
|
|
136
89
|
* Which affordance positions are actually insertable at `nodeId` (EC-405 scope 3):
|
|
137
90
|
* a position is offered only when it structurally exists AND at least one registry
|
|
138
|
-
* component may land there (the SAME
|
|
139
|
-
*
|
|
91
|
+
* component may land there (the SAME judge the tree/DnD, the blocks panel and the
|
|
92
|
+
* reducer apply — {@link hasInsertableComponent} over the resolved `allow`), so a
|
|
140
93
|
* blocked spot shows no dead button.
|
|
141
94
|
*
|
|
142
95
|
* An EMPTY, insertable container surfaces ONLY `into` — the meaningful action there
|
|
@@ -146,17 +99,17 @@ export function resolveInsertContextAt(builder, nodeId, position, fieldAllow, st
|
|
|
146
99
|
* child already carries its own edge affordances, so a filled container needs no
|
|
147
100
|
* center "+").
|
|
148
101
|
*/
|
|
149
|
-
export function insertablePositions(builder, nodeId,
|
|
102
|
+
export function insertablePositions(builder, nodeId, studio = getStudioRegistry()) {
|
|
150
103
|
// Empty insertable container → into only.
|
|
151
|
-
const into = resolveInsertContextAt(builder, nodeId, 'into',
|
|
152
|
-
if (into && (into.target.index ?? 0) === 0 && hasInsertableComponent(studio, into.
|
|
104
|
+
const into = resolveInsertContextAt(builder, nodeId, 'into', studio);
|
|
105
|
+
if (into && (into.target.index ?? 0) === 0 && hasInsertableComponent(studio, into.allow)) {
|
|
153
106
|
return ['into'];
|
|
154
107
|
}
|
|
155
108
|
// Otherwise the sibling edges that accept something.
|
|
156
109
|
const out = [];
|
|
157
110
|
for (const position of ['before', 'after']) {
|
|
158
|
-
const ctx = resolveInsertContextAt(builder, nodeId, position,
|
|
159
|
-
if (ctx && hasInsertableComponent(studio, ctx.
|
|
111
|
+
const ctx = resolveInsertContextAt(builder, nodeId, position, studio);
|
|
112
|
+
if (ctx && hasInsertableComponent(studio, ctx.allow))
|
|
160
113
|
out.push(position);
|
|
161
114
|
}
|
|
162
115
|
return out;
|