@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
|
@@ -1,26 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo, useReducer } from 'react';
|
|
3
|
-
import { cloneWithNewIds, duplicateNode, getNode, getParentInfo,
|
|
4
|
-
import {
|
|
2
|
+
import { useMemo, useReducer, useState } from 'react';
|
|
3
|
+
import { DEFAULT_SLOT, cloneWithNewIds, duplicateNode, getNode, getParentInfo, gridContainerNode, insertNode, moveNode, normalizeContainers, removeNode, removeNodeProp, reorderNode, replaceNode, replacePlacementRefusal, setNodeProp, subtreePlacementRefusal, } from '@elytracms/core/project-graph';
|
|
4
|
+
import { createUuidIdFactory } from '@elytracms/core/persistence';
|
|
5
|
+
import { WRAP_GRID_OPTIONS, applyWrap } from '@elytracms/studio/lib/dnd/drop-rules';
|
|
6
|
+
import { replaceRun, runByKey } from './canvas-runs.js';
|
|
5
7
|
import { createRequiredContext } from '@elytracms/studio/lib/required-context';
|
|
6
8
|
const HISTORY_LIMIT = 100;
|
|
7
|
-
/**
|
|
8
|
-
* The single fresh-node-id counter (`node-<n>`), shared by the reducer's structural
|
|
9
|
-
* actions AND the clipboard paste path (`clipboard-actions.tsx`) so the paste-issue
|
|
10
|
-
* deep links can mirror the exact ids the reducer will mint — one implementation,
|
|
11
|
-
* no drift. `count` mutates as `genId` is called, so the caller reads it afterward
|
|
12
|
-
* to advance `nodeSeq`.
|
|
13
|
-
*/
|
|
14
|
-
export function makeNodeIdCounter(start) {
|
|
15
|
-
const c = {
|
|
16
|
-
count: start,
|
|
17
|
-
genId: () => {
|
|
18
|
-
c.count += 1;
|
|
19
|
-
return `node-${c.count}`;
|
|
20
|
-
},
|
|
21
|
-
};
|
|
22
|
-
return c;
|
|
23
|
-
}
|
|
24
9
|
/**
|
|
25
10
|
* Push a new graph onto history, coalescing consecutive edits with the same key.
|
|
26
11
|
*
|
|
@@ -35,67 +20,130 @@ export function makeNodeIdCounter(start) {
|
|
|
35
20
|
function commit(state, root, opts = {}) {
|
|
36
21
|
const normalized = normalizeContainers(root, state.layout);
|
|
37
22
|
const present = normalized;
|
|
23
|
+
const preserved = opts.preserved ?? state.preserved;
|
|
38
24
|
const coalesce = opts.coalesceKey != null && opts.coalesceKey === state.lastCoalesceKey;
|
|
39
25
|
// Normalization can dissolve the node a caller wanted selected (e.g. the
|
|
40
26
|
// parent container of a removed child) — selection never points at a ghost.
|
|
41
27
|
const desired = opts.selectedId !== undefined ? opts.selectedId : state.selectedId;
|
|
42
28
|
const selectedId = desired !== null && getNode(normalized, desired) ? desired : null;
|
|
29
|
+
const desiredRun = opts.selectedRun !== undefined ? opts.selectedRun : state.selectedRun;
|
|
43
30
|
return {
|
|
44
31
|
...state,
|
|
45
|
-
past: coalesce ? state.past : [...state.past, state
|
|
32
|
+
past: coalesce ? state.past : [...state.past, snapshotOf(state)].slice(-HISTORY_LIMIT),
|
|
46
33
|
present,
|
|
34
|
+
preserved,
|
|
47
35
|
future: [],
|
|
48
|
-
nodeSeq: opts.nodeSeq ?? state.nodeSeq,
|
|
49
36
|
lastCoalesceKey: opts.coalesceKey ?? null,
|
|
50
37
|
selectedId,
|
|
38
|
+
selectedRun: liveRun(present, preserved, desiredRun),
|
|
39
|
+
refusal: null,
|
|
51
40
|
};
|
|
52
41
|
}
|
|
42
|
+
function snapshotOf(state) {
|
|
43
|
+
return { root: state.present, preserved: state.preserved };
|
|
44
|
+
}
|
|
45
|
+
/** The run key if that run still exists in this state, else `null` (never a ghost selection). */
|
|
46
|
+
function liveRun(root, preserved, run) {
|
|
47
|
+
if (run === null)
|
|
48
|
+
return null;
|
|
49
|
+
return runByKey(rootCount(root), preserved, run) ? run : null;
|
|
50
|
+
}
|
|
51
|
+
/** How many component roots the composition has (the synthetic root's children). */
|
|
52
|
+
function rootCount(root) {
|
|
53
|
+
return root.slots?.[DEFAULT_SLOT]?.length ?? 0;
|
|
54
|
+
}
|
|
55
|
+
/** Restore a history entry (undo/redo), keeping a selection only where it still exists. */
|
|
56
|
+
function restore(state, to, past, future) {
|
|
57
|
+
return {
|
|
58
|
+
...state,
|
|
59
|
+
past,
|
|
60
|
+
present: to.root,
|
|
61
|
+
preserved: to.preserved,
|
|
62
|
+
future,
|
|
63
|
+
lastCoalesceKey: null,
|
|
64
|
+
selectedId: getNode(to.root, state.selectedId ?? '') ? state.selectedId : null,
|
|
65
|
+
selectedRun: liveRun(to.root, to.preserved, state.selectedRun),
|
|
66
|
+
refusal: null,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Leave the composition untouched and say why (EC-584 — never a silent no-op). */
|
|
70
|
+
function refuse(state, action, verdict) {
|
|
71
|
+
return { ...state, refusal: { action: action.type, verdict } };
|
|
72
|
+
}
|
|
53
73
|
/**
|
|
54
|
-
* The pure state transition
|
|
55
|
-
*
|
|
56
|
-
*
|
|
74
|
+
* The builder's pure state transition, CONSTRUCTED with the placement scope it judges
|
|
75
|
+
* by (EC-584, owner-ratified 4a): the field's effective top-level vocabulary and the
|
|
76
|
+
* manifest lookup. The scope is static per store and deliberately not reducer STATE
|
|
77
|
+
* (it is the declaration the tree is judged against, not part of the tree) — the
|
|
78
|
+
* reducer RECEIVES it, so there is no `(state, action)` to call without it.
|
|
79
|
+
*
|
|
80
|
+
* Every structural action that places a node — `insert`, `replace`, `move`, `wrap`,
|
|
81
|
+
* `duplicate` — runs the ONE core judge (`subtreePlacementRefusal`) over every node
|
|
82
|
+
* that enters or moves, BEFORE committing. A refusal returns the composition
|
|
83
|
+
* unchanged, with the verdict in {@link BuilderState.refusal}: so no gesture — a
|
|
84
|
+
* forgotten pre-check, promote, detach, a fixture's `slotChildren`, a server-only
|
|
85
|
+
* paste — can place what the field, a slot or canvas eligibility forbids, and the
|
|
86
|
+
* publish gate (which calls the same judge) cannot disagree with the canvas.
|
|
87
|
+
*
|
|
88
|
+
* Exported (with `initialBuilderState`) for unit tests — gesture determinism and
|
|
89
|
+
* normalization lockstep are reducer-level properties, not render behavior.
|
|
57
90
|
*/
|
|
58
|
-
export function
|
|
91
|
+
export function createBuilderReducer(placement) {
|
|
92
|
+
return (state, action) => transition(state, action, placement);
|
|
93
|
+
}
|
|
94
|
+
function transition(state, action, placement) {
|
|
59
95
|
const root = state.present;
|
|
60
96
|
switch (action.type) {
|
|
61
97
|
case 'select':
|
|
62
|
-
return { ...state, selectedId: action.id };
|
|
98
|
+
return { ...state, selectedId: action.id, selectedRun: action.id === null ? state.selectedRun : null, refusal: null };
|
|
99
|
+
case 'selectRun':
|
|
100
|
+
return {
|
|
101
|
+
...state,
|
|
102
|
+
selectedRun: liveRun(root, state.preserved, action.run),
|
|
103
|
+
selectedId: action.run === null ? state.selectedId : null,
|
|
104
|
+
refusal: null,
|
|
105
|
+
};
|
|
106
|
+
case 'removeRun': {
|
|
107
|
+
const run = runByKey(rootCount(root), state.preserved, action.run);
|
|
108
|
+
if (!run)
|
|
109
|
+
return state;
|
|
110
|
+
return commit(state, root, { preserved: replaceRun(state.preserved, run, []), selectedRun: null });
|
|
111
|
+
}
|
|
112
|
+
case 'setRun': {
|
|
113
|
+
const run = runByKey(rootCount(root), state.preserved, action.run);
|
|
114
|
+
if (!run)
|
|
115
|
+
return state;
|
|
116
|
+
return commit(state, root, {
|
|
117
|
+
preserved: replaceRun(state.preserved, run, action.content),
|
|
118
|
+
coalesceKey: action.coalesceKey,
|
|
119
|
+
selectedRun: action.content.length > 0 ? action.run : null,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
63
122
|
case 'hover':
|
|
64
123
|
return state.hoveredId === action.id ? state : { ...state, hoveredId: action.id };
|
|
65
124
|
case 'viewport':
|
|
66
125
|
return { ...state, viewport: action.viewport };
|
|
126
|
+
case 'clearRefusal':
|
|
127
|
+
return state.refusal === null ? state : { ...state, refusal: null };
|
|
67
128
|
case 'undo': {
|
|
68
129
|
const prev = state.past.at(-1);
|
|
69
130
|
if (!prev)
|
|
70
131
|
return state;
|
|
71
|
-
return
|
|
72
|
-
...state,
|
|
73
|
-
past: state.past.slice(0, -1),
|
|
74
|
-
present: prev,
|
|
75
|
-
future: [state.present, ...state.future],
|
|
76
|
-
lastCoalesceKey: null,
|
|
77
|
-
selectedId: getNode(prev, state.selectedId ?? '') ? state.selectedId : null,
|
|
78
|
-
};
|
|
132
|
+
return restore(state, prev, state.past.slice(0, -1), [snapshotOf(state), ...state.future]);
|
|
79
133
|
}
|
|
80
134
|
case 'redo': {
|
|
81
135
|
const next = state.future[0];
|
|
82
136
|
if (!next)
|
|
83
137
|
return state;
|
|
84
|
-
return
|
|
85
|
-
...state,
|
|
86
|
-
past: [...state.past, state.present],
|
|
87
|
-
present: next,
|
|
88
|
-
future: state.future.slice(1),
|
|
89
|
-
lastCoalesceKey: null,
|
|
90
|
-
selectedId: getNode(next, state.selectedId ?? '') ? state.selectedId : null,
|
|
91
|
-
};
|
|
138
|
+
return restore(state, next, [...state.past, snapshotOf(state)], state.future.slice(1));
|
|
92
139
|
}
|
|
93
140
|
case 'insert': {
|
|
94
|
-
const
|
|
95
|
-
const
|
|
141
|
+
const node = action.minted ? action.node : cloneWithNewIds(action.node, state.newId);
|
|
142
|
+
const verdict = subtreePlacementRefusal(root, node, landingOf(action.target), placement);
|
|
143
|
+
if (verdict)
|
|
144
|
+
return refuse(state, action, verdict);
|
|
96
145
|
const next = insertNode(root, action.target, node);
|
|
97
146
|
return commit(state, next, {
|
|
98
|
-
nodeSeq: c.count,
|
|
99
147
|
selectedId: action.select === false ? state.selectedId : node.id,
|
|
100
148
|
});
|
|
101
149
|
}
|
|
@@ -106,25 +154,49 @@ export function builderReducer(state, action) {
|
|
|
106
154
|
return commit(state, next, { selectedId });
|
|
107
155
|
}
|
|
108
156
|
case 'duplicate': {
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
157
|
+
// The copy (and every node of its subtree) ENTERS beside the original.
|
|
158
|
+
const info = getParentInfo(root, action.id);
|
|
159
|
+
const source = getNode(root, action.id);
|
|
160
|
+
if (info && source) {
|
|
161
|
+
const verdict = subtreePlacementRefusal(root, source, { parentId: info.parent.id, slot: info.slot }, placement);
|
|
162
|
+
if (verdict)
|
|
163
|
+
return refuse(state, action, verdict);
|
|
164
|
+
}
|
|
165
|
+
const { root: next, newId } = duplicateNode(root, action.id, state.newId);
|
|
166
|
+
return commit(state, next, { selectedId: newId ?? state.selectedId });
|
|
112
167
|
}
|
|
113
168
|
case 'reorder':
|
|
169
|
+
// Same parent, same slot: the placement does not change — nothing to judge.
|
|
114
170
|
return commit(state, reorderNode(root, action.id, action.delta));
|
|
115
|
-
case 'move':
|
|
171
|
+
case 'move': {
|
|
172
|
+
// A node that changes parent or slot lands somewhere new (its descendants keep
|
|
173
|
+
// their own slots, so only it is judged); a move within its own slot is a
|
|
174
|
+
// reorder and leaves the placement as it was.
|
|
175
|
+
const info = getParentInfo(root, action.id);
|
|
176
|
+
const node = getNode(root, action.id);
|
|
177
|
+
const relocates = !info || info.parent.id !== action.target.parentId || info.slot !== action.target.slot;
|
|
178
|
+
if (node && relocates) {
|
|
179
|
+
const verdict = subtreePlacementRefusal(root, withoutSlots(node), landingOf(action.target), placement);
|
|
180
|
+
if (verdict)
|
|
181
|
+
return refuse(state, action, verdict);
|
|
182
|
+
}
|
|
116
183
|
return commit(state, moveNode(root, action.id, action.target));
|
|
184
|
+
}
|
|
117
185
|
case 'replace': {
|
|
118
186
|
// Mint fresh ids for the incoming nodes (page-local namespace) so a detached
|
|
119
187
|
// copy never collides with the page, and a promoted block's source subtree
|
|
120
188
|
// (kept for the new reusable document) stays untouched.
|
|
121
|
-
const
|
|
122
|
-
|
|
189
|
+
const cloned = action.nodes.map((n) => cloneWithNewIds(n, state.newId));
|
|
190
|
+
// EC-560: promote (one embed) and detach (the reusable's content) land where the
|
|
191
|
+
// replaced node was — judged there like any insert (the reuse preflight asks the
|
|
192
|
+
// same `replacePlacementRefusal` before any side effect).
|
|
193
|
+
const verdict = replacePlacementRefusal(root, action.id, cloned, placement);
|
|
194
|
+
if (verdict)
|
|
195
|
+
return refuse(state, action, verdict);
|
|
123
196
|
const next = replaceNode(root, action.id, cloned);
|
|
124
197
|
if (next === root)
|
|
125
198
|
return state; // root or absent id — no-op
|
|
126
199
|
return commit(state, next, {
|
|
127
|
-
nodeSeq: c.count,
|
|
128
200
|
selectedId: action.select === false ? state.selectedId : (cloned[0]?.id ?? state.selectedId),
|
|
129
201
|
});
|
|
130
202
|
}
|
|
@@ -133,16 +205,28 @@ export function builderReducer(state, action) {
|
|
|
133
205
|
// the two columns of a new Grid container — ONE structural edit, one
|
|
134
206
|
// history entry, one mirrored command, so a single undo dissolves the
|
|
135
207
|
// whole gesture.
|
|
136
|
-
const c = makeNodeIdCounter(state.nodeSeq);
|
|
137
208
|
const partner = action.subject.kind === 'node'
|
|
138
209
|
? { kind: 'node', nodeId: action.subject.nodeId }
|
|
139
|
-
: { kind: 'subtree', node: cloneWithNewIds(action.subject.node,
|
|
210
|
+
: { kind: 'subtree', node: cloneWithNewIds(action.subject.node, state.newId) };
|
|
140
211
|
// EC-591: the gesture builds the `layout.grid` component; with none, it is off
|
|
141
212
|
// (the drop rules refuse it before dispatch — this guard keeps the reducer total).
|
|
142
213
|
const gridComponentId = state.layout.grid;
|
|
143
214
|
if (gridComponentId === undefined)
|
|
144
215
|
return state;
|
|
145
|
-
|
|
216
|
+
// EC-584: the Grid lands where the reference is; the reference and the partner
|
|
217
|
+
// land in the Grid's column slot. A moved partner keeps its own subtree (only it
|
|
218
|
+
// relocates); a new one (a blocks-panel tile) enters whole.
|
|
219
|
+
const reference = getNode(root, action.referenceId);
|
|
220
|
+
const referenceInfo = getParentInfo(root, action.referenceId);
|
|
221
|
+
const partnerNode = partner.kind === 'node' ? getNode(root, partner.nodeId) : partner.node;
|
|
222
|
+
if (reference && referenceInfo && partnerNode) {
|
|
223
|
+
const columns = [withoutSlots(reference), partner.kind === 'node' ? withoutSlots(partnerNode) : partnerNode];
|
|
224
|
+
const probe = gridContainerNode(gridComponentId, 'wrap', WRAP_GRID_OPTIONS, columns);
|
|
225
|
+
const verdict = subtreePlacementRefusal(root, probe, { parentId: referenceInfo.parent.id, slot: referenceInfo.slot }, placement);
|
|
226
|
+
if (verdict)
|
|
227
|
+
return refuse(state, action, verdict);
|
|
228
|
+
}
|
|
229
|
+
const containerId = state.newId();
|
|
146
230
|
const wrapped = applyWrap({
|
|
147
231
|
root,
|
|
148
232
|
gridComponentId,
|
|
@@ -153,7 +237,7 @@ export function builderReducer(state, action) {
|
|
|
153
237
|
});
|
|
154
238
|
if (!wrapped)
|
|
155
239
|
return state;
|
|
156
|
-
return commit(state, wrapped.root, {
|
|
240
|
+
return commit(state, wrapped.root, { selectedId: containerId });
|
|
157
241
|
}
|
|
158
242
|
case 'setProp':
|
|
159
243
|
return commit(state, setNodeProp(root, action.id, action.name, action.value), {
|
|
@@ -165,8 +249,18 @@ export function builderReducer(state, action) {
|
|
|
165
249
|
return state;
|
|
166
250
|
}
|
|
167
251
|
}
|
|
252
|
+
function landingOf(target) {
|
|
253
|
+
return { parentId: target.parentId, slot: target.slot };
|
|
254
|
+
}
|
|
255
|
+
/** The node alone — a move relocates it; its descendants keep their own slots. */
|
|
256
|
+
function withoutSlots(node) {
|
|
257
|
+
const { slots: _slots, ...rest } = node;
|
|
258
|
+
return rest;
|
|
259
|
+
}
|
|
168
260
|
const { Context: BuilderContext, useRequired: useBuilder } = createRequiredContext('Builder');
|
|
169
|
-
export function initialBuilderState(composition, layout
|
|
261
|
+
export function initialBuilderState(composition, layout, newId,
|
|
262
|
+
/** EC-549 — the field value's typography items with their anchors (`splitBlockRoots`). */
|
|
263
|
+
preserved = []) {
|
|
170
264
|
// Start container-normal (idempotent on healthy trees) so the session and the
|
|
171
265
|
// lifted baseline agree from the first edit.
|
|
172
266
|
const present = normalizeContainers(composition, layout);
|
|
@@ -174,15 +268,17 @@ export function initialBuilderState(composition, layout) {
|
|
|
174
268
|
past: [],
|
|
175
269
|
present,
|
|
176
270
|
future: [],
|
|
271
|
+
preserved,
|
|
177
272
|
selectedId: present.id,
|
|
273
|
+
selectedRun: null,
|
|
178
274
|
hoveredId: null,
|
|
179
275
|
viewport: 'full',
|
|
180
|
-
//
|
|
181
|
-
// tree
|
|
182
|
-
|
|
183
|
-
nodeSeq: maxNodeIdSeq(present),
|
|
276
|
+
// EC-578: fresh ids come from the injected factory — never a counter seeded past
|
|
277
|
+
// the tree's `node-<n>` ids (EC-203), which two sessions advanced identically.
|
|
278
|
+
newId,
|
|
184
279
|
lastCoalesceKey: null,
|
|
185
280
|
layout,
|
|
281
|
+
refusal: null,
|
|
186
282
|
};
|
|
187
283
|
}
|
|
188
284
|
/**
|
|
@@ -193,12 +289,17 @@ export function initialBuilderState(composition, layout) {
|
|
|
193
289
|
* its committed composition into the open CMS document via `onChange`
|
|
194
290
|
* (`composition-field-editor`); the document's own Save persists it as content.
|
|
195
291
|
*/
|
|
196
|
-
export function BuilderProvider({ composition, field, layout, children, }) {
|
|
292
|
+
export function BuilderProvider({ composition, preserved, field, components, layout, idFactory, children, }) {
|
|
293
|
+
const [newId] = useState(() => idFactory ?? createUuidIdFactory());
|
|
294
|
+
// EC-584: the reducer is constructed ONCE with the scope it judges by — the field
|
|
295
|
+
// and the lookup are static for the store's lifetime (a remount re-reads them).
|
|
296
|
+
const [placement] = useState(() => ({ components, fieldAllow: field.allow }));
|
|
297
|
+
const [reducer] = useState(() => createBuilderReducer(placement));
|
|
197
298
|
// EC-287: nothing persists a graph — committed edits are lifted into the CMS
|
|
198
299
|
// document by the composition-field editor's `onChange`. `useReducer` drives the
|
|
199
|
-
// pure
|
|
200
|
-
//
|
|
201
|
-
const [state, dispatch] = useReducer(
|
|
300
|
+
// pure reducer (consecutive dispatches in one gesture apply in sequence); the
|
|
301
|
+
// store below decorates the state with the derived undo/redo + selection flags.
|
|
302
|
+
const [state, dispatch] = useReducer(reducer, { composition, layout, newId, preserved }, (init) => initialBuilderState(init.composition, init.layout, init.newId, init.preserved));
|
|
202
303
|
const store = useMemo(() => {
|
|
203
304
|
const selectedNode = state.selectedId
|
|
204
305
|
? (getNode(state.present, state.selectedId) ?? null)
|
|
@@ -209,9 +310,10 @@ export function BuilderProvider({ composition, field, layout, children, }) {
|
|
|
209
310
|
canRedo: state.future.length > 0,
|
|
210
311
|
selectedNode,
|
|
211
312
|
field,
|
|
313
|
+
placement,
|
|
212
314
|
dispatch,
|
|
213
315
|
};
|
|
214
|
-
}, [state, field, dispatch]);
|
|
316
|
+
}, [state, field, placement, dispatch]);
|
|
215
317
|
return _jsx(BuilderContext.Provider, { value: store, children: children });
|
|
216
318
|
}
|
|
217
319
|
export { useBuilder };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builder-store.js","sourceRoot":"","sources":["../../../src/components/builder/builder-store.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useReducer } from 'react'\nimport type { ReactNode } from 'react'\nimport {\n cloneWithNewIds,\n duplicateNode,\n getNode,\n getParentInfo,\n insertNode,\n maxNodeIdSeq,\n moveNode,\n normalizeContainers,\n removeNode,\n removeNodeProp,\n reorderNode,\n replaceNode,\n setNodeProp,\n} from '@elytracms/core/project-graph'\nimport type { ComponentNode, InsertTarget, LayoutContainerIds, PropValue } from '@elytracms/core/project-graph'\nimport { applyWrap } from '@elytracms/studio/lib/dnd/drop-rules'\nimport { createRequiredContext } from '@elytracms/studio/lib/required-context'\n\nconst HISTORY_LIMIT = 100\n\n/**\n * A canvas preview-width key (EC-250): `'full'` (unconstrained) or a configured\n * breakpoint name (`'sm'`, `'md'`, …). Dynamic because the set is sourced from\n * the consuming site's breakpoints, so it is a string, not a fixed union — the\n * switcher resolves the available options via {@link resolveCanvasViewports}.\n */\nexport type Viewport = string\n\n/**\n * The builder store's action union. EC-287 retired the DB-resident project graph\n * and its persistence mirror, so these actions are pure in-memory editor state —\n * no action maps to a persisted `graph.*` command anymore. The block-field editor\n * lifts committed edits into the CMS document via `onChange`\n * (`composition-field-editor`); nothing persists a graph.\n */\nexport type BuilderAction =\n | { type: 'select'; id: string | null }\n | { type: 'hover'; id: string | null }\n | { type: 'viewport'; viewport: Viewport }\n | { type: 'undo' }\n | { type: 'redo' }\n | { type: 'insert'; target: InsertTarget; node: ComponentNode; select?: boolean }\n | { type: 'remove'; id: string }\n | { type: 'duplicate'; id: string }\n | { type: 'reorder'; id: string; delta: -1 | 1 }\n | { type: 'move'; id: string; target: InsertTarget }\n /**\n * EC-282 reuse gesture: replace a node with zero or more nodes at its\n * position (clone-with-fresh-ids, like `insert`). Promote → 1 embed node;\n * detach → the referenced element's inline blocks. One atomic structural\n * edit, one undo step.\n */\n | { type: 'replace'; id: string; nodes: ComponentNode[]; select?: boolean }\n /**\n * EC-162 layout gesture: wrap the reference node and the dragged subject in\n * a two-column Grid container as ONE atomic structural edit (one undo step).\n */\n | {\n type: 'wrap'\n referenceId: string\n side: 'left' | 'right'\n subject: { kind: 'node'; nodeId: string } | { kind: 'component'; node: ComponentNode }\n }\n | { type: 'setProp'; id: string; name: string; value: PropValue; coalesceKey?: string }\n | { type: 'removeProp'; id: string; name: string }\n\nexport interface BuilderState {\n /**\n * The edited composition: one root-bearing component tree (EC-287). The canvas\n * wraps a page document's `body` blocks into a synthetic root and edits it\n * directly — no page/layout wrapper. Undo/redo snapshot this root.\n */\n past: ComponentNode[]\n present: ComponentNode\n future: ComponentNode[]\n selectedId: string | null\n hoveredId: string | null\n viewport: Viewport\n /** Monotonic counter feeding fresh node ids. */\n nodeSeq: number\n /** Coalescing key of the last edit (consecutive same-key edits don't stack history). */\n lastCoalesceKey: string | null\n /**\n * The components filling the layout roles (EC-591, `ComponentRegistry.layout`),\n * fixed for the store's lifetime: which nodes container normalization treats as\n * Stack/Grid, and the Grid the wrap gesture builds. No hardcoded engine id.\n */\n layout: LayoutContainerIds\n}\n\ntype Action = BuilderAction\n\nexport interface Counter {\n genId: () => string\n count: number\n}\n/**\n * The single fresh-node-id counter (`node-<n>`), shared by the reducer's structural\n * actions AND the clipboard paste path (`clipboard-actions.tsx`) so the paste-issue\n * deep links can mirror the exact ids the reducer will mint — one implementation,\n * no drift. `count` mutates as `genId` is called, so the caller reads it afterward\n * to advance `nodeSeq`.\n */\nexport function makeNodeIdCounter(start: number): Counter {\n const c: Counter = {\n count: start,\n genId: () => {\n c.count += 1\n return `node-${c.count}`\n },\n }\n return c\n}\n\n/**\n * Push a new graph onto history, coalescing consecutive edits with the same key.\n *\n * Every commit normalizes layout containers (EC-162 ledger fix): the\n * operations layer normalizes every persisted `graph.*` write (EC-161), so\n * the optimistic working graph must apply the same `normalizeContainers` pass\n * or the UI state diverges from the persisted revision after\n * container-emptying moves/removes. The mirrored command stays un-normalized\n * (`graphInvocationFor` sees the raw action); both sides normalize\n * identically, so they land on the same graph.\n */\nfunction commit(\n state: BuilderState,\n root: ComponentNode,\n opts: { coalesceKey?: string; nodeSeq?: number; selectedId?: string | null } = {},\n): BuilderState {\n const normalized = normalizeContainers(root, state.layout)\n const present = normalized\n const coalesce = opts.coalesceKey != null && opts.coalesceKey === state.lastCoalesceKey\n // Normalization can dissolve the node a caller wanted selected (e.g. the\n // parent container of a removed child) — selection never points at a ghost.\n const desired = opts.selectedId !== undefined ? opts.selectedId : state.selectedId\n const selectedId = desired !== null && getNode(normalized, desired) ? desired : null\n return {\n ...state,\n past: coalesce ? state.past : [...state.past, state.present].slice(-HISTORY_LIMIT),\n present,\n future: [],\n nodeSeq: opts.nodeSeq ?? state.nodeSeq,\n lastCoalesceKey: opts.coalesceKey ?? null,\n selectedId,\n }\n}\n\n/**\n * The pure state transition. Exported (with `initialBuilderState`) for unit\n * tests — gesture determinism and normalization lockstep are reducer-level\n * properties, not render behavior.\n */\nexport function builderReducer(state: BuilderState, action: Action): BuilderState {\n const root = state.present\n switch (action.type) {\n case 'select':\n return { ...state, selectedId: action.id }\n case 'hover':\n return state.hoveredId === action.id ? state : { ...state, hoveredId: action.id }\n case 'viewport':\n return { ...state, viewport: action.viewport }\n\n case 'undo': {\n const prev = state.past.at(-1)\n if (!prev) return state\n return {\n ...state,\n past: state.past.slice(0, -1),\n present: prev,\n future: [state.present, ...state.future],\n lastCoalesceKey: null,\n selectedId: getNode(prev, state.selectedId ?? '') ? state.selectedId : null,\n }\n }\n case 'redo': {\n const next = state.future[0]\n if (!next) return state\n return {\n ...state,\n past: [...state.past, state.present],\n present: next,\n future: state.future.slice(1),\n lastCoalesceKey: null,\n selectedId: getNode(next, state.selectedId ?? '') ? state.selectedId : null,\n }\n }\n\n case 'insert': {\n const c = makeNodeIdCounter(state.nodeSeq)\n const node = cloneWithNewIds(action.node, c.genId)\n const next = insertNode(root, action.target, node)\n return commit(state, next, {\n nodeSeq: c.count,\n selectedId: action.select === false ? state.selectedId : node.id,\n })\n }\n case 'remove': {\n const parent = getParentInfo(root, action.id)\n const next = removeNode(root, action.id)\n const selectedId =\n state.selectedId === action.id ? (parent?.parent.id ?? null) : state.selectedId\n return commit(state, next, { selectedId })\n }\n case 'duplicate': {\n const c = makeNodeIdCounter(state.nodeSeq)\n const { root: next, newId } = duplicateNode(root, action.id, c.genId)\n return commit(state, next, { nodeSeq: c.count, selectedId: newId ?? state.selectedId })\n }\n case 'reorder':\n return commit(state, reorderNode(root, action.id, action.delta))\n case 'move':\n return commit(state, moveNode(root, action.id, action.target))\n case 'replace': {\n // Mint fresh ids for the incoming nodes (page-local namespace) so a detached\n // copy never collides with the page, and a promoted block's source subtree\n // (kept for the new reusable document) stays untouched.\n const c = makeNodeIdCounter(state.nodeSeq)\n const cloned = action.nodes.map((n) => cloneWithNewIds(n, c.genId))\n const next = replaceNode(root, action.id, cloned)\n if (next === root) return state // root or absent id — no-op\n return commit(state, next, {\n nodeSeq: c.count,\n selectedId: action.select === false ? state.selectedId : (cloned[0]?.id ?? state.selectedId),\n })\n }\n case 'wrap': {\n // Layout gesture (EC-162): the reference and the dragged subject become\n // the two columns of a new Grid container — ONE structural edit, one\n // history entry, one mirrored command, so a single undo dissolves the\n // whole gesture.\n const c = makeNodeIdCounter(state.nodeSeq)\n const partner =\n action.subject.kind === 'node'\n ? { kind: 'node' as const, nodeId: action.subject.nodeId }\n : { kind: 'subtree' as const, node: cloneWithNewIds(action.subject.node, c.genId) }\n // EC-591: the gesture builds the `layout.grid` component; with none, it is off\n // (the drop rules refuse it before dispatch — this guard keeps the reducer total).\n const gridComponentId = state.layout.grid\n if (gridComponentId === undefined) return state\n const containerId = c.genId()\n const wrapped = applyWrap({\n root,\n gridComponentId,\n referenceId: action.referenceId,\n side: action.side,\n partner,\n containerId,\n })\n if (!wrapped) return state\n return commit(state, wrapped.root, { nodeSeq: c.count, selectedId: containerId })\n }\n case 'setProp':\n return commit(state, setNodeProp(root, action.id, action.name, action.value), {\n coalesceKey: action.coalesceKey,\n })\n case 'removeProp':\n return commit(state, removeNodeProp(root, action.id, action.name))\n default:\n return state\n }\n}\n\n/**\n * The composition FIELD whose value the store edits (EC-554). Static for the\n * store's lifetime and deliberately NOT reducer state: it is the declaration the\n * edited tree is judged against, not part of the tree. It travels WITH the store\n * so every placement surface under the provider — the canvas and page-tree\n * drag-and-drop, paste (canvas, tree, keyboard), and the insert offer — reads the\n * same value from the one context it already requires; none can be mounted\n * without it, and `BuilderProvider` cannot be mounted without declaring it.\n */\nexport interface CompositionFieldScope {\n /** The field's display label — what a refusal notice names (e.g. \"Canvas\"). */\n label: string\n /**\n * The field's top-level `allow` vocabulary (EC-188; a closed set per EC-544),\n * or `undefined` when the field declares none (any registered component).\n * Applied only through `fieldAllowAt` (`lib/dnd/drop-rules`), which scopes it\n * to direct children of the composition root.\n */\n allow: readonly string[] | undefined\n}\n\nexport interface BuilderStore extends BuilderState {\n canUndo: boolean\n canRedo: boolean\n selectedNode: ComponentNode | null\n /** The composition field being edited (EC-554) — see {@link CompositionFieldScope}. */\n field: CompositionFieldScope\n dispatch: (action: Action) => void\n}\n\nconst { Context: BuilderContext, useRequired: useBuilder } =\n createRequiredContext<BuilderStore>('Builder')\n\nexport function initialBuilderState(composition: ComponentNode, layout: LayoutContainerIds): BuilderState {\n // Start container-normal (idempotent on healthy trees) so the session and the\n // lifted baseline agree from the first edit.\n const present = normalizeContainers(composition, layout)\n return {\n past: [],\n present,\n future: [],\n selectedId: present.id,\n hoveredId: null,\n viewport: 'full',\n // Seed past the ids the composition already carries (EC-203): a restored\n // tree holds minted `node-<n>` ids, so starting at 0 would re-mint `node-1`\n // on the first insert and produce a duplicate-node-id.\n nodeSeq: maxNodeIdSeq(present),\n lastCoalesceKey: null,\n layout,\n }\n}\n\n/**\n * Undo/redo design (EC-141): the history stack is *UI state* — an in-memory\n * stack of composition-root snapshots. EC-287 retired the DB-resident project\n * graph and its append-only revision log, so the store no longer mirrors edits onto a\n * persistence layer: it is a pure in-memory editor. The block-field editor lifts\n * its committed composition into the open CMS document via `onChange`\n * (`composition-field-editor`); the document's own Save persists it as content.\n */\nexport function BuilderProvider({\n composition,\n field,\n layout,\n children,\n}: {\n /** The composition root to edit (the composition-field editor's synthetic root). */\n composition: ComponentNode\n /** The field the composition is the value of (EC-554) — required, never defaulted. */\n field: CompositionFieldScope\n /** The layout-role component ids (EC-591, `ComponentRegistry.layout`) — required. */\n layout: LayoutContainerIds\n children: ReactNode\n}) {\n // EC-287: nothing persists a graph — committed edits are lifted into the CMS\n // document by the composition-field editor's `onChange`. `useReducer` drives the\n // pure `builderReducer` (consecutive dispatches in one gesture apply in sequence);\n // the store below decorates the state with the derived undo/redo + selection flags.\n const [state, dispatch] = useReducer(builderReducer, { composition, layout }, (init) =>\n initialBuilderState(init.composition, init.layout),\n )\n\n const store = useMemo<BuilderStore>(() => {\n const selectedNode = state.selectedId\n ? (getNode(state.present, state.selectedId) ?? null)\n : null\n return {\n ...state,\n canUndo: state.past.length > 0,\n canRedo: state.future.length > 0,\n selectedNode,\n field,\n dispatch,\n }\n }, [state, field, dispatch])\n\n return <BuilderContext.Provider value={store}>{children}</BuilderContext.Provider>\n}\n\nexport { useBuilder }\n"]}
|
|
1
|
+
{"version":3,"file":"builder-store.js","sourceRoot":"","sources":["../../../src/components/builder/builder-store.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useReducer, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport {\n DEFAULT_SLOT,\n cloneWithNewIds,\n duplicateNode,\n getNode,\n getParentInfo,\n gridContainerNode,\n insertNode,\n moveNode,\n normalizeContainers,\n removeNode,\n removeNodeProp,\n reorderNode,\n replaceNode,\n replacePlacementRefusal,\n setNodeProp,\n subtreePlacementRefusal,\n} from '@elytracms/core/project-graph'\nimport type {\n AllowList,\n ComponentLookup,\n ComponentNode,\n InsertTarget,\n LayoutContainerIds,\n PlacementScope,\n PlacementVerdict,\n PropValue,\n} from '@elytracms/core/project-graph'\nimport { createUuidIdFactory } from '@elytracms/core/persistence'\nimport type { IdFactory } from '@elytracms/core/persistence'\nimport { WRAP_GRID_OPTIONS, applyWrap } from '@elytracms/studio/lib/dnd/drop-rules'\nimport { replaceRun, runByKey } from './canvas-runs'\nimport type { PreservedItem } from './canvas-runs'\nimport { createRequiredContext } from '@elytracms/studio/lib/required-context'\n\nconst HISTORY_LIMIT = 100\n\n/**\n * A canvas preview-width key (EC-250): `'full'` (unconstrained) or a configured\n * breakpoint name (`'sm'`, `'md'`, …). Dynamic because the set is sourced from\n * the consuming site's breakpoints, so it is a string, not a fixed union — the\n * switcher resolves the available options via {@link resolveCanvasViewports}.\n */\nexport type Viewport = string\n\n/**\n * The builder store's action union. EC-287 retired the DB-resident project graph\n * and its persistence mirror, so these actions are pure in-memory editor state —\n * no action maps to a persisted `graph.*` command anymore. The block-field editor\n * lifts committed edits into the CMS document via `onChange`\n * (`composition-field-editor`); nothing persists a graph.\n */\nexport type BuilderAction =\n | { type: 'select'; id: string | null }\n | { type: 'hover'; id: string | null }\n | { type: 'viewport'; viewport: Viewport }\n | { type: 'undo' }\n | { type: 'redo' }\n /**\n * Insert a node (its subtree re-id'd from the store's id factory). `minted` — the\n * node ALREADY carries fresh ids from the same factory (the paste path mints first\n * so its issue deep links name exactly the ids that land); the reducer then inserts\n * it as given.\n */\n | { type: 'insert'; target: InsertTarget; node: ComponentNode; select?: boolean; minted?: true }\n | { type: 'remove'; id: string }\n | { type: 'duplicate'; id: string }\n | { type: 'reorder'; id: string; delta: -1 | 1 }\n | { type: 'move'; id: string; target: InsertTarget }\n /**\n * EC-282 reuse gesture: replace a node with zero or more nodes at its\n * position (clone-with-fresh-ids, like `insert`). Promote → 1 embed node;\n * detach → the referenced element's inline blocks. One atomic structural\n * edit, one undo step.\n */\n | { type: 'replace'; id: string; nodes: ComponentNode[]; select?: boolean }\n /**\n * EC-162 layout gesture: wrap the reference node and the dragged subject in\n * a two-column Grid container as ONE atomic structural edit (one undo step).\n */\n | {\n type: 'wrap'\n referenceId: string\n side: 'left' | 'right'\n subject: { kind: 'node'; nodeId: string } | { kind: 'component'; node: ComponentNode }\n }\n | { type: 'setProp'; id: string; name: string; value: PropValue; coalesceKey?: string }\n | { type: 'removeProp'; id: string; name: string }\n /** EC-584: the editor dismissed the visible refusal ({@link BuilderState.refusal}). */\n | { type: 'clearRefusal' }\n /**\n * EC-549 — select a typography RUN by its key (`canvas-runs.ts`: the index of its\n * first preserved item); `null` clears it. A run and a node are never both selected.\n */\n | { type: 'selectRun'; run: number | null }\n /** EC-549 — delete a typography run: a real, undoable edit of the preserved list. */\n | { type: 'removeRun'; run: number }\n /**\n * EC-549 — replace a run's typography with `content` (the inspector's rich-text\n * editor on that run; consecutive same-key edits coalesce into one undo step). Empty\n * `content` deletes the run.\n */\n | { type: 'setRun'; run: number; content: readonly unknown[]; coalesceKey?: string }\n\n/** The structural actions the placement judge rules on (EC-584): every node that enters or moves. */\nexport type PlacingAction = Extract<BuilderAction, { type: 'insert' | 'replace' | 'move' | 'wrap' | 'duplicate' }>\n\n/** A refused placement (EC-584): the judge's verdict and the action it refused. */\nexport interface PlacementRefusal {\n action: PlacingAction['type']\n verdict: PlacementVerdict\n}\n\n/**\n * One history entry (EC-549): the component tree AND the preserved typography — so a\n * run's deletion or edit is undone exactly like a block's.\n */\nexport interface CanvasSnapshot {\n root: ComponentNode\n preserved: readonly PreservedItem[]\n}\n\nexport interface BuilderState {\n /**\n * The edited composition: one root-bearing component tree (EC-287). The canvas\n * wraps a page document's `body` blocks into a synthetic root and edits it\n * directly — no page/layout wrapper. Undo/redo snapshot it with the preserved\n * typography ({@link CanvasSnapshot}).\n */\n past: CanvasSnapshot[]\n present: ComponentNode\n future: CanvasSnapshot[]\n /**\n * EC-549 — the field value's typography items (no ids, EC-500 D1), each anchored by\n * how many component roots preceded it (`componentPrefix`); the field value is\n * `mergeBlockRoots(present's roots, preserved)`. In the store, so selecting,\n * deleting and editing a run is a real action with truthful undo.\n */\n preserved: readonly PreservedItem[]\n selectedId: string | null\n /** EC-549 — the selected typography run's key, or `null` (exclusive with {@link selectedId}). */\n selectedRun: number | null\n hoveredId: string | null\n viewport: Viewport\n /**\n * EC-578 / EC-595 A2 — THE node-id factory (injected; UUIDs in the studio, a\n * sequential factory in fixtures). It replaced the per-session `node-<n>` counter,\n * which two sessions editing one page advanced identically — both minted the same\n * next id, a `duplicate-node-id` on the second save. Stored ids are untouched.\n */\n newId: IdFactory\n /** Coalescing key of the last edit (consecutive same-key edits don't stack history). */\n lastCoalesceKey: string | null\n /**\n * The components filling the layout roles (EC-591, `ComponentRegistry.layout`),\n * fixed for the store's lifetime: which nodes container normalization treats as\n * Stack/Grid, and the Grid the wrap gesture builds. No hardcoded engine id.\n */\n layout: LayoutContainerIds\n /**\n * EC-584 — the last REFUSED placement, with its reason: the reducer judged an\n * `insert`/`replace`/`move`/`wrap`/`duplicate` against the field and slot\n * vocabularies and canvas eligibility (the core placement judge) and left the\n * composition unchanged. Transient — the next edit, selection, undo or redo clears\n * it — and never silent: the canvas shows it (`PlacementRefusalNotice`). A refused\n * action changes no tree, so it adds no history entry and emits no canvas patch.\n */\n refusal: PlacementRefusal | null\n}\n\ntype Action = BuilderAction\n\n/**\n * Push a new graph onto history, coalescing consecutive edits with the same key.\n *\n * Every commit normalizes layout containers (EC-162 ledger fix): the\n * operations layer normalizes every persisted `graph.*` write (EC-161), so\n * the optimistic working graph must apply the same `normalizeContainers` pass\n * or the UI state diverges from the persisted revision after\n * container-emptying moves/removes. The mirrored command stays un-normalized\n * (`graphInvocationFor` sees the raw action); both sides normalize\n * identically, so they land on the same graph.\n */\nfunction commit(\n state: BuilderState,\n root: ComponentNode,\n opts: {\n coalesceKey?: string\n selectedId?: string | null\n preserved?: readonly PreservedItem[]\n selectedRun?: number | null\n } = {},\n): BuilderState {\n const normalized = normalizeContainers(root, state.layout)\n const present = normalized\n const preserved = opts.preserved ?? state.preserved\n const coalesce = opts.coalesceKey != null && opts.coalesceKey === state.lastCoalesceKey\n // Normalization can dissolve the node a caller wanted selected (e.g. the\n // parent container of a removed child) — selection never points at a ghost.\n const desired = opts.selectedId !== undefined ? opts.selectedId : state.selectedId\n const selectedId = desired !== null && getNode(normalized, desired) ? desired : null\n const desiredRun = opts.selectedRun !== undefined ? opts.selectedRun : state.selectedRun\n return {\n ...state,\n past: coalesce ? state.past : [...state.past, snapshotOf(state)].slice(-HISTORY_LIMIT),\n present,\n preserved,\n future: [],\n lastCoalesceKey: opts.coalesceKey ?? null,\n selectedId,\n selectedRun: liveRun(present, preserved, desiredRun),\n refusal: null,\n }\n}\n\nfunction snapshotOf(state: BuilderState): CanvasSnapshot {\n return { root: state.present, preserved: state.preserved }\n}\n\n/** The run key if that run still exists in this state, else `null` (never a ghost selection). */\nfunction liveRun(root: ComponentNode, preserved: readonly PreservedItem[], run: number | null): number | null {\n if (run === null) return null\n return runByKey(rootCount(root), preserved, run) ? run : null\n}\n\n/** How many component roots the composition has (the synthetic root's children). */\nfunction rootCount(root: ComponentNode): number {\n return root.slots?.[DEFAULT_SLOT]?.length ?? 0\n}\n\n/** Restore a history entry (undo/redo), keeping a selection only where it still exists. */\nfunction restore(state: BuilderState, to: CanvasSnapshot, past: CanvasSnapshot[], future: CanvasSnapshot[]): BuilderState {\n return {\n ...state,\n past,\n present: to.root,\n preserved: to.preserved,\n future,\n lastCoalesceKey: null,\n selectedId: getNode(to.root, state.selectedId ?? '') ? state.selectedId : null,\n selectedRun: liveRun(to.root, to.preserved, state.selectedRun),\n refusal: null,\n }\n}\n\n/** Leave the composition untouched and say why (EC-584 — never a silent no-op). */\nfunction refuse(state: BuilderState, action: PlacingAction, verdict: PlacementVerdict): BuilderState {\n return { ...state, refusal: { action: action.type, verdict } }\n}\n\n/**\n * The builder's pure state transition, CONSTRUCTED with the placement scope it judges\n * by (EC-584, owner-ratified 4a): the field's effective top-level vocabulary and the\n * manifest lookup. The scope is static per store and deliberately not reducer STATE\n * (it is the declaration the tree is judged against, not part of the tree) — the\n * reducer RECEIVES it, so there is no `(state, action)` to call without it.\n *\n * Every structural action that places a node — `insert`, `replace`, `move`, `wrap`,\n * `duplicate` — runs the ONE core judge (`subtreePlacementRefusal`) over every node\n * that enters or moves, BEFORE committing. A refusal returns the composition\n * unchanged, with the verdict in {@link BuilderState.refusal}: so no gesture — a\n * forgotten pre-check, promote, detach, a fixture's `slotChildren`, a server-only\n * paste — can place what the field, a slot or canvas eligibility forbids, and the\n * publish gate (which calls the same judge) cannot disagree with the canvas.\n *\n * Exported (with `initialBuilderState`) for unit tests — gesture determinism and\n * normalization lockstep are reducer-level properties, not render behavior.\n */\nexport function createBuilderReducer(\n placement: PlacementScope,\n): (state: BuilderState, action: Action) => BuilderState {\n return (state, action) => transition(state, action, placement)\n}\n\nfunction transition(state: BuilderState, action: Action, placement: PlacementScope): BuilderState {\n const root = state.present\n switch (action.type) {\n case 'select':\n return { ...state, selectedId: action.id, selectedRun: action.id === null ? state.selectedRun : null, refusal: null }\n case 'selectRun':\n return {\n ...state,\n selectedRun: liveRun(root, state.preserved, action.run),\n selectedId: action.run === null ? state.selectedId : null,\n refusal: null,\n }\n case 'removeRun': {\n const run = runByKey(rootCount(root), state.preserved, action.run)\n if (!run) return state\n return commit(state, root, { preserved: replaceRun(state.preserved, run, []), selectedRun: null })\n }\n case 'setRun': {\n const run = runByKey(rootCount(root), state.preserved, action.run)\n if (!run) return state\n return commit(state, root, {\n preserved: replaceRun(state.preserved, run, action.content),\n coalesceKey: action.coalesceKey,\n selectedRun: action.content.length > 0 ? action.run : null,\n })\n }\n case 'hover':\n return state.hoveredId === action.id ? state : { ...state, hoveredId: action.id }\n case 'viewport':\n return { ...state, viewport: action.viewport }\n case 'clearRefusal':\n return state.refusal === null ? state : { ...state, refusal: null }\n\n case 'undo': {\n const prev = state.past.at(-1)\n if (!prev) return state\n return restore(state, prev, state.past.slice(0, -1), [snapshotOf(state), ...state.future])\n }\n case 'redo': {\n const next = state.future[0]\n if (!next) return state\n return restore(state, next, [...state.past, snapshotOf(state)], state.future.slice(1))\n }\n\n case 'insert': {\n const node = action.minted ? action.node : cloneWithNewIds(action.node, state.newId)\n const verdict = subtreePlacementRefusal(root, node, landingOf(action.target), placement)\n if (verdict) return refuse(state, action, verdict)\n const next = insertNode(root, action.target, node)\n return commit(state, next, {\n selectedId: action.select === false ? state.selectedId : node.id,\n })\n }\n case 'remove': {\n const parent = getParentInfo(root, action.id)\n const next = removeNode(root, action.id)\n const selectedId =\n state.selectedId === action.id ? (parent?.parent.id ?? null) : state.selectedId\n return commit(state, next, { selectedId })\n }\n case 'duplicate': {\n // The copy (and every node of its subtree) ENTERS beside the original.\n const info = getParentInfo(root, action.id)\n const source = getNode(root, action.id)\n if (info && source) {\n const verdict = subtreePlacementRefusal(root, source, { parentId: info.parent.id, slot: info.slot }, placement)\n if (verdict) return refuse(state, action, verdict)\n }\n const { root: next, newId } = duplicateNode(root, action.id, state.newId)\n return commit(state, next, { selectedId: newId ?? state.selectedId })\n }\n case 'reorder':\n // Same parent, same slot: the placement does not change — nothing to judge.\n return commit(state, reorderNode(root, action.id, action.delta))\n case 'move': {\n // A node that changes parent or slot lands somewhere new (its descendants keep\n // their own slots, so only it is judged); a move within its own slot is a\n // reorder and leaves the placement as it was.\n const info = getParentInfo(root, action.id)\n const node = getNode(root, action.id)\n const relocates = !info || info.parent.id !== action.target.parentId || info.slot !== action.target.slot\n if (node && relocates) {\n const verdict = subtreePlacementRefusal(root, withoutSlots(node), landingOf(action.target), placement)\n if (verdict) return refuse(state, action, verdict)\n }\n return commit(state, moveNode(root, action.id, action.target))\n }\n case 'replace': {\n // Mint fresh ids for the incoming nodes (page-local namespace) so a detached\n // copy never collides with the page, and a promoted block's source subtree\n // (kept for the new reusable document) stays untouched.\n const cloned = action.nodes.map((n) => cloneWithNewIds(n, state.newId))\n // EC-560: promote (one embed) and detach (the reusable's content) land where the\n // replaced node was — judged there like any insert (the reuse preflight asks the\n // same `replacePlacementRefusal` before any side effect).\n const verdict = replacePlacementRefusal(root, action.id, cloned, placement)\n if (verdict) return refuse(state, action, verdict)\n const next = replaceNode(root, action.id, cloned)\n if (next === root) return state // root or absent id — no-op\n return commit(state, next, {\n selectedId: action.select === false ? state.selectedId : (cloned[0]?.id ?? state.selectedId),\n })\n }\n case 'wrap': {\n // Layout gesture (EC-162): the reference and the dragged subject become\n // the two columns of a new Grid container — ONE structural edit, one\n // history entry, one mirrored command, so a single undo dissolves the\n // whole gesture.\n const partner =\n action.subject.kind === 'node'\n ? { kind: 'node' as const, nodeId: action.subject.nodeId }\n : { kind: 'subtree' as const, node: cloneWithNewIds(action.subject.node, state.newId) }\n // EC-591: the gesture builds the `layout.grid` component; with none, it is off\n // (the drop rules refuse it before dispatch — this guard keeps the reducer total).\n const gridComponentId = state.layout.grid\n if (gridComponentId === undefined) return state\n // EC-584: the Grid lands where the reference is; the reference and the partner\n // land in the Grid's column slot. A moved partner keeps its own subtree (only it\n // relocates); a new one (a blocks-panel tile) enters whole.\n const reference = getNode(root, action.referenceId)\n const referenceInfo = getParentInfo(root, action.referenceId)\n const partnerNode = partner.kind === 'node' ? getNode(root, partner.nodeId) : partner.node\n if (reference && referenceInfo && partnerNode) {\n const columns = [withoutSlots(reference), partner.kind === 'node' ? withoutSlots(partnerNode) : partnerNode]\n const probe = gridContainerNode(gridComponentId, 'wrap', WRAP_GRID_OPTIONS, columns)\n const verdict = subtreePlacementRefusal(\n root,\n probe,\n { parentId: referenceInfo.parent.id, slot: referenceInfo.slot },\n placement,\n )\n if (verdict) return refuse(state, action, verdict)\n }\n const containerId = state.newId()\n const wrapped = applyWrap({\n root,\n gridComponentId,\n referenceId: action.referenceId,\n side: action.side,\n partner,\n containerId,\n })\n if (!wrapped) return state\n return commit(state, wrapped.root, { selectedId: containerId })\n }\n case 'setProp':\n return commit(state, setNodeProp(root, action.id, action.name, action.value), {\n coalesceKey: action.coalesceKey,\n })\n case 'removeProp':\n return commit(state, removeNodeProp(root, action.id, action.name))\n default:\n return state\n }\n}\n\nfunction landingOf(target: InsertTarget): { parentId: string; slot?: string } {\n return { parentId: target.parentId, slot: target.slot }\n}\n\n/** The node alone — a move relocates it; its descendants keep their own slots. */\nfunction withoutSlots(node: ComponentNode): ComponentNode {\n const { slots: _slots, ...rest } = node\n return rest\n}\n\n/**\n * The composition FIELD whose value the store edits (EC-554). Static for the\n * store's lifetime and deliberately NOT reducer state: it is the declaration the\n * edited tree is judged against, not part of the tree. It travels WITH the store\n * so every placement surface under the provider — the canvas and page-tree\n * drag-and-drop, paste (canvas, tree, keyboard), and the insert offer — reads the\n * same value from the one context it already requires; none can be mounted\n * without it, and `BuilderProvider` cannot be mounted without declaring it.\n */\nexport interface CompositionFieldScope {\n /** The field's display label — what a refusal notice names (e.g. \"Canvas\"). */\n label: string\n /**\n * The field's EFFECTIVE top-level vocabulary (EC-188; EC-584 — `fieldAllowList(field)`:\n * its `allow`, else the `blocks` type's default). The judge applies it only to\n * direct children of the composition root (`allowAt`, core `placement.ts`).\n */\n allow: AllowList\n}\n\nexport interface BuilderStore extends BuilderState {\n canUndo: boolean\n canRedo: boolean\n selectedNode: ComponentNode | null\n /** The composition field being edited (EC-554) — see {@link CompositionFieldScope}. */\n field: CompositionFieldScope\n /**\n * EC-584 — what the reducer judges every placement by: the manifest lookup and the\n * field's vocabulary. The gestures' own pre-checks (drag-and-drop, paste, the insert\n * offer, the reuse preflight) read the SAME scope, so an indicator and the sink agree.\n */\n placement: PlacementScope\n dispatch: (action: Action) => void\n}\n\nconst { Context: BuilderContext, useRequired: useBuilder } =\n createRequiredContext<BuilderStore>('Builder')\n\nexport function initialBuilderState(\n composition: ComponentNode,\n layout: LayoutContainerIds,\n newId: IdFactory,\n /** EC-549 — the field value's typography items with their anchors (`splitBlockRoots`). */\n preserved: readonly PreservedItem[] = [],\n): BuilderState {\n // Start container-normal (idempotent on healthy trees) so the session and the\n // lifted baseline agree from the first edit.\n const present = normalizeContainers(composition, layout)\n return {\n past: [],\n present,\n future: [],\n preserved,\n selectedId: present.id,\n selectedRun: null,\n hoveredId: null,\n viewport: 'full',\n // EC-578: fresh ids come from the injected factory — never a counter seeded past\n // the tree's `node-<n>` ids (EC-203), which two sessions advanced identically.\n newId,\n lastCoalesceKey: null,\n layout,\n refusal: null,\n }\n}\n\n/**\n * Undo/redo design (EC-141): the history stack is *UI state* — an in-memory\n * stack of composition-root snapshots. EC-287 retired the DB-resident project\n * graph and its append-only revision log, so the store no longer mirrors edits onto a\n * persistence layer: it is a pure in-memory editor. The block-field editor lifts\n * its committed composition into the open CMS document via `onChange`\n * (`composition-field-editor`); the document's own Save persists it as content.\n */\nexport function BuilderProvider({\n composition,\n preserved,\n field,\n components,\n layout,\n idFactory,\n children,\n}: {\n /** The composition root to edit (the composition-field editor's synthetic root). */\n composition: ComponentNode\n /** EC-549 — the field value's typography items (`splitBlockRoots(value).preserved`). */\n preserved?: readonly PreservedItem[]\n /** The field the composition is the value of (EC-554) — required, never defaulted. */\n field: CompositionFieldScope\n /** The manifest lookup the placement judge reads (EC-584, `registry.lookup`) — required. */\n components: ComponentLookup\n /** The layout-role component ids (EC-591, `ComponentRegistry.layout`) — required. */\n layout: LayoutContainerIds\n /**\n * EC-578 — the node-id factory. Defaults to UUIDs (`createUuidIdFactory`, one per\n * store); fixtures inject a deterministic factory.\n */\n idFactory?: IdFactory\n children: ReactNode\n}) {\n const [newId] = useState<IdFactory>(() => idFactory ?? createUuidIdFactory())\n // EC-584: the reducer is constructed ONCE with the scope it judges by — the field\n // and the lookup are static for the store's lifetime (a remount re-reads them).\n const [placement] = useState<PlacementScope>(() => ({ components, fieldAllow: field.allow }))\n const [reducer] = useState(() => createBuilderReducer(placement))\n // EC-287: nothing persists a graph — committed edits are lifted into the CMS\n // document by the composition-field editor's `onChange`. `useReducer` drives the\n // pure reducer (consecutive dispatches in one gesture apply in sequence); the\n // store below decorates the state with the derived undo/redo + selection flags.\n const [state, dispatch] = useReducer(reducer, { composition, layout, newId, preserved }, (init) =>\n initialBuilderState(init.composition, init.layout, init.newId, init.preserved),\n )\n\n const store = useMemo<BuilderStore>(() => {\n const selectedNode = state.selectedId\n ? (getNode(state.present, state.selectedId) ?? null)\n : null\n return {\n ...state,\n canUndo: state.past.length > 0,\n canRedo: state.future.length > 0,\n selectedNode,\n field,\n placement,\n dispatch,\n }\n }, [state, field, placement, dispatch])\n\n return <BuilderContext.Provider value={store}>{children}</BuilderContext.Provider>\n}\n\nexport { useBuilder }\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ComponentNode } from '@elytracms/core/project-graph';
|
|
3
|
+
import type { RenderContext } from '@elytracms/core/runtime-renderer';
|
|
4
|
+
import type { PreservedItem } from './canvas-runs.js';
|
|
5
|
+
/**
|
|
6
|
+
* What the canvas iframe renders (EC-592 + EC-549, owner-ratified 4d) — pure, so the
|
|
7
|
+
* markup is pinned by `renderToStaticMarkup`.
|
|
8
|
+
*
|
|
9
|
+
* The canvas renders the FIELD VALUE — the store's component roots merged with its
|
|
10
|
+
* preserved typography (`mergeBlockRoots`, the same merge the write path stores) —
|
|
11
|
+
* through delivery's ONE entry point (`renderDocumentBodyItems(documentBodyItems(…))`),
|
|
12
|
+
* with the context core's `createRenderContext` built. So the canvas and the live page
|
|
13
|
+
* cannot diverge: typography runs paint in their true positions through the same
|
|
14
|
+
* rich-text mapping, nested values keep their prose, and blocks render as bare
|
|
15
|
+
* siblings exactly as on delivery.
|
|
16
|
+
*
|
|
17
|
+
* The editor's synthetic root is DECORATED, never rendered as a component: it is the
|
|
18
|
+
* selectable, droppable frame around the items (`data-ec-node-id` of the root, the
|
|
19
|
+
* `ec-editable` wrapper every canvas gesture resolves against) — no Stack styling the
|
|
20
|
+
* stored value does not carry. Each top-level typography run is decorated by its PATH
|
|
21
|
+
* (`data-ec-run` = the run's key, `data-ec-run-start`/`-end` = its index range in the
|
|
22
|
+
* field value), so it can be selected and deleted like a block (EC-549).
|
|
23
|
+
*/
|
|
24
|
+
export declare function renderCanvasBody(args: {
|
|
25
|
+
root: ComponentNode;
|
|
26
|
+
preserved: readonly PreservedItem[];
|
|
27
|
+
ctx: RenderContext;
|
|
28
|
+
selectedId: string | null;
|
|
29
|
+
selectedRun: number | null;
|
|
30
|
+
}): ReactNode;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { DEFAULT_SLOT, mergeBlockRoots } from '@elytracms/core/project-graph';
|
|
3
|
+
import { documentBodyItems, renderDocumentBodyItems } from '@elytracms/core/runtime-renderer';
|
|
4
|
+
import { canvasRuns } from './canvas-runs.js';
|
|
5
|
+
/**
|
|
6
|
+
* What the canvas iframe renders (EC-592 + EC-549, owner-ratified 4d) — pure, so the
|
|
7
|
+
* markup is pinned by `renderToStaticMarkup`.
|
|
8
|
+
*
|
|
9
|
+
* The canvas renders the FIELD VALUE — the store's component roots merged with its
|
|
10
|
+
* preserved typography (`mergeBlockRoots`, the same merge the write path stores) —
|
|
11
|
+
* through delivery's ONE entry point (`renderDocumentBodyItems(documentBodyItems(…))`),
|
|
12
|
+
* with the context core's `createRenderContext` built. So the canvas and the live page
|
|
13
|
+
* cannot diverge: typography runs paint in their true positions through the same
|
|
14
|
+
* rich-text mapping, nested values keep their prose, and blocks render as bare
|
|
15
|
+
* siblings exactly as on delivery.
|
|
16
|
+
*
|
|
17
|
+
* The editor's synthetic root is DECORATED, never rendered as a component: it is the
|
|
18
|
+
* selectable, droppable frame around the items (`data-ec-node-id` of the root, the
|
|
19
|
+
* `ec-editable` wrapper every canvas gesture resolves against) — no Stack styling the
|
|
20
|
+
* stored value does not carry. Each top-level typography run is decorated by its PATH
|
|
21
|
+
* (`data-ec-run` = the run's key, `data-ec-run-start`/`-end` = its index range in the
|
|
22
|
+
* field value), so it can be selected and deleted like a block (EC-549).
|
|
23
|
+
*/
|
|
24
|
+
export function renderCanvasBody(args) {
|
|
25
|
+
const { root, preserved, ctx } = args;
|
|
26
|
+
const roots = root.slots?.[DEFAULT_SLOT] ?? [];
|
|
27
|
+
const value = mergeBlockRoots(roots, preserved);
|
|
28
|
+
const runs = canvasRuns(roots.length, preserved);
|
|
29
|
+
return (_jsx("div", { "data-ec-node-id": root.id, "data-ec-canvas-root": "", className: "ec-editable", "data-selected": root.id === args.selectedId || undefined, children: renderDocumentBodyItems(documentBodyItems(value), ctx, {
|
|
30
|
+
decorateRegion: (region, rendered) => {
|
|
31
|
+
const run = region.range ? runs.find((candidate) => candidate.range.start === region.range.start) : undefined;
|
|
32
|
+
if (!run)
|
|
33
|
+
return rendered;
|
|
34
|
+
return (_jsx("div", { "data-ec-run": run.key, "data-ec-run-start": run.range.start, "data-ec-run-end": run.range.end, className: "ec-run", "data-selected": run.key === args.selectedRun || undefined, children: rendered }));
|
|
35
|
+
},
|
|
36
|
+
}) }));
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=canvas-body.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canvas-body.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-body.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAA;AAE7E,OAAO,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAA;AAE7F,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAG1C;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAMhC;IACC,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;IACrC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,CAAA;IAC9C,MAAM,KAAK,GAAG,eAAe,CAAC,KAAK,EAAE,SAAS,CAAC,CAAA;IAC/C,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;IAChD,OAAO,CACL,iCACmB,IAAI,CAAC,EAAE,yBACJ,EAAE,EACtB,SAAS,EAAC,aAAa,mBACR,IAAI,CAAC,EAAE,KAAK,IAAI,CAAC,UAAU,IAAI,SAAS,YAEtD,uBAAuB,CAAC,iBAAiB,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE;YACtD,cAAc,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE;gBACnC,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,KAAK,KAAK,MAAM,CAAC,KAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;gBAC9G,IAAI,CAAC,GAAG;oBAAE,OAAO,QAAQ,CAAA;gBACzB,OAAO,CACL,6BACe,GAAG,CAAC,GAAG,uBACD,GAAG,CAAC,KAAK,CAAC,KAAK,qBACjB,GAAG,CAAC,KAAK,CAAC,GAAG,EAC9B,SAAS,EAAC,QAAQ,mBACH,GAAG,CAAC,GAAG,KAAK,IAAI,CAAC,WAAW,IAAI,SAAS,YAEvD,QAAQ,GACL,CACP,CAAA;YACH,CAAC;SACF,CAAC,GACE,CACP,CAAA;AACH,CAAC","sourcesContent":["import type { ReactNode } from 'react'\nimport { DEFAULT_SLOT, mergeBlockRoots } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { documentBodyItems, renderDocumentBodyItems } from '@elytracms/core/runtime-renderer'\nimport type { RenderContext } from '@elytracms/core/runtime-renderer'\nimport { canvasRuns } from './canvas-runs'\nimport type { PreservedItem } from './canvas-runs'\n\n/**\n * What the canvas iframe renders (EC-592 + EC-549, owner-ratified 4d) — pure, so the\n * markup is pinned by `renderToStaticMarkup`.\n *\n * The canvas renders the FIELD VALUE — the store's component roots merged with its\n * preserved typography (`mergeBlockRoots`, the same merge the write path stores) —\n * through delivery's ONE entry point (`renderDocumentBodyItems(documentBodyItems(…))`),\n * with the context core's `createRenderContext` built. So the canvas and the live page\n * cannot diverge: typography runs paint in their true positions through the same\n * rich-text mapping, nested values keep their prose, and blocks render as bare\n * siblings exactly as on delivery.\n *\n * The editor's synthetic root is DECORATED, never rendered as a component: it is the\n * selectable, droppable frame around the items (`data-ec-node-id` of the root, the\n * `ec-editable` wrapper every canvas gesture resolves against) — no Stack styling the\n * stored value does not carry. Each top-level typography run is decorated by its PATH\n * (`data-ec-run` = the run's key, `data-ec-run-start`/`-end` = its index range in the\n * field value), so it can be selected and deleted like a block (EC-549).\n */\nexport function renderCanvasBody(args: {\n root: ComponentNode\n preserved: readonly PreservedItem[]\n ctx: RenderContext\n selectedId: string | null\n selectedRun: number | null\n}): ReactNode {\n const { root, preserved, ctx } = args\n const roots = root.slots?.[DEFAULT_SLOT] ?? []\n const value = mergeBlockRoots(roots, preserved)\n const runs = canvasRuns(roots.length, preserved)\n return (\n <div\n data-ec-node-id={root.id}\n data-ec-canvas-root=\"\"\n className=\"ec-editable\"\n data-selected={root.id === args.selectedId || undefined}\n >\n {renderDocumentBodyItems(documentBodyItems(value), ctx, {\n decorateRegion: (region, rendered) => {\n const run = region.range ? runs.find((candidate) => candidate.range.start === region.range!.start) : undefined\n if (!run) return rendered\n return (\n <div\n data-ec-run={run.key}\n data-ec-run-start={run.range.start}\n data-ec-run-end={run.range.end}\n className=\"ec-run\"\n data-selected={run.key === args.selectedRun || undefined}\n >\n {rendered}\n </div>\n )\n },\n })}\n </div>\n )\n}\n"]}
|
|
@@ -88,6 +88,13 @@ const CHROME_UI_CSS = `
|
|
|
88
88
|
it); its !important (contract item 3) keeps a site theme's element rules
|
|
89
89
|
from hiding those states. */
|
|
90
90
|
.ec-editable { outline-offset: -1px !important; cursor: default; }
|
|
91
|
+
/* EC-549: a typography RUN of the field value (loose prose between blocks) —
|
|
92
|
+
selectable and deletable by its path. Prose flows in the page, so the selected
|
|
93
|
+
run carries its own outline (a run has no frame bar: it is edited and deleted
|
|
94
|
+
in the inspector, or with the Delete key). */
|
|
95
|
+
.ec-run { cursor: default; }
|
|
96
|
+
.ec-run:hover { outline: 1px dashed #93b4ff !important; outline-offset: 2px !important; }
|
|
97
|
+
.ec-run[data-selected] { outline: 2px solid #5b8cff !important; outline-offset: 2px !important; }
|
|
91
98
|
|
|
92
99
|
/* ── Selection/hover overlay (EC-404) ───────────────────────────────────
|
|
93
100
|
The topmost editing layer, portaled into the iframe body as a sibling of
|