@elytracms/studio 0.0.14 → 0.0.15
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/cms.ts +8 -31
- package/convex/content.ts +3 -44
- package/convex/delivery.ts +8 -42
- package/convex/functions.ts +17 -2
- package/convex/members.ts +2 -2
- package/convex/referenceMaintenance.ts +4 -73
- package/convex/references.ts +4 -2
- package/convex/schema.ts +17 -37
- package/convex/sync.ts +10 -42
- package/convex/triggers.ts +57 -0
- package/convex/webhooks.ts +3 -2
- package/dist/components/builder/builder-store.d.ts +85 -39
- package/dist/components/builder/builder-store.js +29 -125
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/canvas.js +23 -55
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/clipboard-actions.js +3 -3
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor.js +13 -23
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/insert-context.js +1 -1
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/page-tree.js +3 -4
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/property-inspector.js +4 -64
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/cms/cell.d.ts +0 -4
- package/dist/components/cms/cell.js +1 -5
- package/dist/components/cms/cell.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +1 -1
- package/dist/components/cms/cms-store.js +4 -9
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +1 -1
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/detail-editor.js +2 -2
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/doc-state.d.ts +0 -5
- package/dist/components/cms/doc-state.js +1 -7
- package/dist/components/cms/doc-state.js.map +1 -1
- package/dist/components/cms/editable-cell.js +1 -1
- package/dist/components/cms/editable-cell.js.map +1 -1
- package/dist/components/collab/collab-context.d.ts +6 -14
- package/dist/components/collab/collab-context.js +3 -12
- package/dist/components/collab/collab-context.js.map +1 -1
- package/dist/components/collab/collab-seed.d.ts +7 -19
- package/dist/components/collab/collab-seed.js +9 -31
- package/dist/components/collab/collab-seed.js.map +1 -1
- package/dist/components/components/registry-browser.js +6 -12
- package/dist/components/components/registry-browser.js.map +1 -1
- package/dist/components/guards.d.ts +0 -9
- package/dist/components/guards.js +0 -12
- package/dist/components/guards.js.map +1 -1
- package/dist/components/section-state.d.ts +0 -2
- package/dist/components/section-state.js +0 -9
- package/dist/components/section-state.js.map +1 -1
- package/dist/components/section-surface.d.ts +3 -7
- package/dist/components/section-surface.js +5 -12
- package/dist/components/section-surface.js.map +1 -1
- package/dist/components/sections.d.ts +0 -1
- package/dist/components/sections.js +0 -10
- package/dist/components/sections.js.map +1 -1
- package/dist/components/studio-project-screen.js +13 -14
- package/dist/components/studio-project-screen.js.map +1 -1
- package/dist/components/studio-shell.d.ts +0 -4
- package/dist/components/studio-shell.js +1 -5
- package/dist/components/studio-shell.js.map +1 -1
- package/dist/lib/operations/graph-commands.d.ts +2 -6
- package/dist/lib/operations/graph-commands.js +1 -2
- package/dist/lib/operations/graph-commands.js.map +1 -1
- package/dist/lib/operations/index.d.ts +0 -2
- package/dist/lib/operations/index.js +0 -2
- package/dist/lib/operations/index.js.map +1 -1
- package/dist/lib/operations/session-hydration.d.ts +14 -24
- package/dist/lib/operations/session-hydration.js +15 -34
- package/dist/lib/operations/session-hydration.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.d.ts +1 -3
- package/dist/lib/persistence/convex-adapter.js +1 -154
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persona/labels.js +0 -2
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/references/reference-model.d.ts +0 -16
- package/dist/lib/references/reference-model.js +2 -54
- package/dist/lib/references/reference-model.js.map +1 -1
- package/dist/lib/references/references-store.js +3 -4
- package/dist/lib/references/references-store.js.map +1 -1
- package/dist/lib/registry/index.d.ts +0 -16
- package/dist/lib/registry/index.js +0 -15
- package/dist/lib/registry/index.js.map +1 -1
- package/dist/lib/webhooks/tags.js +2 -16
- package/dist/lib/webhooks/tags.js.map +1 -1
- package/package.json +3 -2
- package/convex/graphs.ts +0 -55
- package/convex/publishing.ts +0 -76
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { CmsDocument } from '@elytracms/core/cms-core'
|
|
2
|
+
import { Triggers } from 'convex-helpers/server/triggers'
|
|
3
|
+
import type { DataModel } from './functions'
|
|
4
|
+
import { maintainDocumentReferences, removeDocumentReferences } from './referenceMaintenance'
|
|
5
|
+
import { scheduleRevalidate } from './revalidate'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* EC-286 — derive at the write chokepoint, not per mutation.
|
|
9
|
+
*
|
|
10
|
+
* Reference-index maintenance (EC-277/AD-6) and host revalidation (EC-146/EC-180)
|
|
11
|
+
* are properties of the Convex **write chokepoint**, not of each mutation. These
|
|
12
|
+
* triggers fire inside the same transaction whenever a content table changes, so
|
|
13
|
+
* EVERY writer is caught **by construction** (principle #4): the studio, the CLI
|
|
14
|
+
* (EC-235), an import, a future automation API, even a raw `ctx.db.patch` in some
|
|
15
|
+
* new mutation that forgot to call a helper. A miss is no longer possible by
|
|
16
|
+
* forgetting — which is exactly how the EC-146 revalidation gap happened (the
|
|
17
|
+
* webhook was wired into graph publish but not document publish).
|
|
18
|
+
*
|
|
19
|
+
* Registered on the `mutation` / `internalMutation` builders in `functions.ts`
|
|
20
|
+
* via `customMutation(…, customCtx(triggers.wrapDB))`. `referenceEntries` has no
|
|
21
|
+
* trigger, so the index writes these derivations perform never recurse.
|
|
22
|
+
*
|
|
23
|
+
* EC-287 retired the DB-resident project graph and its publish path, so this is
|
|
24
|
+
* now the SINGLE `cmsDocuments` trigger — one publish path, the dual-path footgun
|
|
25
|
+
* gone at the source (the EC-146 revalidation gap is now impossible by construction).
|
|
26
|
+
*/
|
|
27
|
+
export const triggers = new Triggers<DataModel>()
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Content documents (EC-224). Every write reindexes the document's outbound
|
|
31
|
+
* references (draft always; published rows while a live version is pinned). Host
|
|
32
|
+
* revalidation fires ONLY when the live pin (`publishedVersion`) actually
|
|
33
|
+
* changes — a plain draft save must not invalidate the published host cache.
|
|
34
|
+
*/
|
|
35
|
+
triggers.register('cmsDocuments', async (ctx, change) => {
|
|
36
|
+
if (change.operation === 'delete') {
|
|
37
|
+
await removeDocumentReferences(ctx, change.oldDoc.collection, change.oldDoc.docId)
|
|
38
|
+
// A published document being deleted drops out of the live site → revalidate.
|
|
39
|
+
if (change.oldDoc.publishedVersion !== undefined) {
|
|
40
|
+
await scheduleRevalidate(ctx, 'documents.delete')
|
|
41
|
+
}
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
const doc = change.newDoc
|
|
45
|
+
await maintainDocumentReferences(
|
|
46
|
+
ctx,
|
|
47
|
+
doc.collection,
|
|
48
|
+
doc.docId,
|
|
49
|
+
doc.document as CmsDocument,
|
|
50
|
+
doc.publishedVersion !== undefined,
|
|
51
|
+
)
|
|
52
|
+
const before = change.oldDoc?.publishedVersion
|
|
53
|
+
const after = doc.publishedVersion
|
|
54
|
+
if (before !== after) {
|
|
55
|
+
await scheduleRevalidate(ctx, after === undefined ? 'documents.unpublish' : 'documents.publish')
|
|
56
|
+
}
|
|
57
|
+
})
|
package/convex/webhooks.ts
CHANGED
|
@@ -7,8 +7,9 @@ import { v } from 'convex/values'
|
|
|
7
7
|
* The v1 dispatcher (`src/lib/webhooks`) runs in the browser next to the
|
|
8
8
|
* operations client and POSTs to the host directly (CORS + bundle-visible
|
|
9
9
|
* secret, documented there). For Convex-backed deployments this action keeps
|
|
10
|
-
* the secret server-side instead: `
|
|
11
|
-
*
|
|
10
|
+
* the secret server-side instead: the `cmsDocuments` write-chokepoint trigger
|
|
11
|
+
* (EC-287: document publish/unpublish is the only publish path) schedules it via
|
|
12
|
+
* `ctx.scheduler.runAfter(0, internal.webhooks.deliver, …)`
|
|
12
13
|
* (EC-180) with the already-computed tag set, and the action signs + POSTs from
|
|
13
14
|
* the Convex runtime. No CORS applies and the secret lives in Convex env config.
|
|
14
15
|
*
|
|
@@ -1,13 +1,81 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import type { ComponentNode,
|
|
3
|
-
import type { BuilderAction, OperationError } from '@elytracms/studio/lib/operations';
|
|
4
|
-
import type { WorkingGraph } from '@elytracms/studio/lib/working-graph';
|
|
2
|
+
import type { ComponentNode, InsertTarget, PropValue } from '@elytracms/core/project-graph';
|
|
5
3
|
export type Viewport = 'desktop' | 'tablet' | 'mobile';
|
|
4
|
+
/**
|
|
5
|
+
* The builder store's action union. EC-287 retired the DB-resident project graph
|
|
6
|
+
* and its persistence mirror, so these actions are pure in-memory editor state —
|
|
7
|
+
* no action maps to a persisted `graph.*` command anymore. The block-field editor
|
|
8
|
+
* lifts committed edits into the CMS document via `onChange`
|
|
9
|
+
* (`composition-field-editor`); nothing persists a graph.
|
|
10
|
+
*/
|
|
11
|
+
export type BuilderAction = {
|
|
12
|
+
type: 'select';
|
|
13
|
+
id: string | null;
|
|
14
|
+
} | {
|
|
15
|
+
type: 'hover';
|
|
16
|
+
id: string | null;
|
|
17
|
+
} | {
|
|
18
|
+
type: 'viewport';
|
|
19
|
+
viewport: Viewport;
|
|
20
|
+
} | {
|
|
21
|
+
type: 'undo';
|
|
22
|
+
} | {
|
|
23
|
+
type: 'redo';
|
|
24
|
+
} | {
|
|
25
|
+
type: 'insert';
|
|
26
|
+
target: InsertTarget;
|
|
27
|
+
node: ComponentNode;
|
|
28
|
+
select?: boolean;
|
|
29
|
+
} | {
|
|
30
|
+
type: 'remove';
|
|
31
|
+
id: string;
|
|
32
|
+
} | {
|
|
33
|
+
type: 'duplicate';
|
|
34
|
+
id: string;
|
|
35
|
+
} | {
|
|
36
|
+
type: 'reorder';
|
|
37
|
+
id: string;
|
|
38
|
+
delta: -1 | 1;
|
|
39
|
+
} | {
|
|
40
|
+
type: 'move';
|
|
41
|
+
id: string;
|
|
42
|
+
target: InsertTarget;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* EC-162 layout gesture: wrap the reference node and the dragged subject in
|
|
46
|
+
* a two-column Grid container as ONE atomic structural edit (one undo step).
|
|
47
|
+
*/
|
|
48
|
+
| {
|
|
49
|
+
type: 'wrap';
|
|
50
|
+
referenceId: string;
|
|
51
|
+
side: 'left' | 'right';
|
|
52
|
+
subject: {
|
|
53
|
+
kind: 'node';
|
|
54
|
+
nodeId: string;
|
|
55
|
+
} | {
|
|
56
|
+
kind: 'component';
|
|
57
|
+
node: ComponentNode;
|
|
58
|
+
};
|
|
59
|
+
} | {
|
|
60
|
+
type: 'setProp';
|
|
61
|
+
id: string;
|
|
62
|
+
name: string;
|
|
63
|
+
value: PropValue;
|
|
64
|
+
coalesceKey?: string;
|
|
65
|
+
} | {
|
|
66
|
+
type: 'removeProp';
|
|
67
|
+
id: string;
|
|
68
|
+
name: string;
|
|
69
|
+
};
|
|
6
70
|
export interface BuilderState {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
71
|
+
/**
|
|
72
|
+
* The edited composition: one root-bearing component tree (EC-287). The canvas
|
|
73
|
+
* wraps a page document's `body` blocks into a synthetic root and edits it
|
|
74
|
+
* directly — no page/layout wrapper. Undo/redo snapshot this root.
|
|
75
|
+
*/
|
|
76
|
+
past: ComponentNode[];
|
|
77
|
+
present: ComponentNode;
|
|
78
|
+
future: ComponentNode[];
|
|
11
79
|
selectedId: string | null;
|
|
12
80
|
hoveredId: string | null;
|
|
13
81
|
viewport: Viewport;
|
|
@@ -28,41 +96,19 @@ export interface BuilderStore extends BuilderState {
|
|
|
28
96
|
canRedo: boolean;
|
|
29
97
|
selectedNode: ComponentNode | null;
|
|
30
98
|
dispatch: (action: Action) => void;
|
|
31
|
-
/**
|
|
32
|
-
* Most recent failed/denied persisted mutation (structured, from the
|
|
33
|
-
* operation result). Rendered by the shell — never a silent failure.
|
|
34
|
-
*/
|
|
35
|
-
persistError: OperationError | null;
|
|
36
|
-
clearPersistError: () => void;
|
|
37
|
-
/** Await all queued persisted mutations (publish flows, tests). */
|
|
38
|
-
flushPersistence: () => Promise<void>;
|
|
39
99
|
}
|
|
40
|
-
export declare function initialBuilderState(
|
|
100
|
+
export declare function initialBuilderState(composition: ComponentNode): BuilderState;
|
|
41
101
|
/**
|
|
42
|
-
* Undo/redo design (EC-141): the history stack
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* (`graph.save`, message "Undo"/"Redo"). This is the honest mapping of a
|
|
49
|
-
* linear session history onto an append-only revision log: the session and
|
|
50
|
-
* the persisted latest revision always agree, and the persisted timeline
|
|
51
|
-
* records that an undo happened rather than pretending it didn't.
|
|
52
|
-
*
|
|
53
|
-
* Without a `projectId` (styleguide, isolated tests) the store is a pure
|
|
54
|
-
* in-memory editor and persists nothing.
|
|
102
|
+
* Undo/redo design (EC-141): the history stack is *UI state* — an in-memory
|
|
103
|
+
* stack of composition-root snapshots. EC-287 retired the DB-resident project
|
|
104
|
+
* graph and its append-only revision log, so the store no longer mirrors edits onto a
|
|
105
|
+
* persistence layer: it is a pure in-memory editor. The block-field editor lifts
|
|
106
|
+
* its committed composition into the open CMS document via `onChange`
|
|
107
|
+
* (`composition-field-editor`); the document's own Save persists it as content.
|
|
55
108
|
*/
|
|
56
|
-
export declare function BuilderProvider({
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* A pre-built working graph (the composition-field editor's synthetic page);
|
|
61
|
-
* used as-is when present, overriding `graph`.
|
|
62
|
-
*/
|
|
63
|
-
workingGraph?: WorkingGraph;
|
|
64
|
-
/** Enables persistence through the shared operations client. */
|
|
65
|
-
projectId?: string;
|
|
109
|
+
export declare function BuilderProvider({ composition, children, }: {
|
|
110
|
+
/** The composition root to edit (the composition-field editor's synthetic root). */
|
|
111
|
+
composition: ComponentNode;
|
|
66
112
|
children: ReactNode;
|
|
67
113
|
}): import("react").JSX.Element;
|
|
68
114
|
export declare function useBuilder(): BuilderStore;
|
|
@@ -1,26 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react';
|
|
3
|
-
import { cloneWithNewIds, duplicateNode, getNode, getParentInfo, insertNode, maxNodeIdSeq, moveNode, normalizeContainers, removeNode, removeNodeProp, reorderNode,
|
|
3
|
+
import { cloneWithNewIds, duplicateNode, getNode, getParentInfo, insertNode, maxNodeIdSeq, moveNode, normalizeContainers, removeNode, removeNodeProp, reorderNode, setNodeProp, } from '@elytracms/core/project-graph';
|
|
4
4
|
import { applyWrap } from '@elytracms/studio/lib/dnd/drop-rules';
|
|
5
|
-
import { createGraphPersister, getStudioOperations } from '@elytracms/studio/lib/operations';
|
|
6
|
-
import { normalizeWorkingGraph, setWorkingPageSeo, toWorkingGraph } from '@elytracms/studio/lib/working-graph';
|
|
7
5
|
const HISTORY_LIMIT = 100;
|
|
8
|
-
function activeRoot(state) {
|
|
9
|
-
const page = state.present.pages.find((p) => p.id === state.pageId);
|
|
10
|
-
return page.root;
|
|
11
|
-
}
|
|
12
|
-
function withRoot(graph, pageId, root) {
|
|
13
|
-
return { ...graph, pages: graph.pages.map((p) => (p.id === pageId ? { ...p, root } : p)) };
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* The highest minted-id sequence already present in the working graph (EC-203).
|
|
17
|
-
* `nodeSeq` is a single session-wide counter feeding inserts on every page, so it
|
|
18
|
-
* must clear the max across ALL pages' roots — never just the active one — or
|
|
19
|
-
* switching pages and inserting would re-mint a collision on another page.
|
|
20
|
-
*/
|
|
21
|
-
function maxGraphNodeSeq(graph) {
|
|
22
|
-
return graph.pages.reduce((max, page) => Math.max(max, maxNodeIdSeq(page.root)), 0);
|
|
23
|
-
}
|
|
24
6
|
function counter(start) {
|
|
25
7
|
const c = {
|
|
26
8
|
count: start,
|
|
@@ -44,7 +26,7 @@ function counter(start) {
|
|
|
44
26
|
*/
|
|
45
27
|
function commit(state, root, opts = {}) {
|
|
46
28
|
const normalized = normalizeContainers(root);
|
|
47
|
-
const present =
|
|
29
|
+
const present = normalized;
|
|
48
30
|
const coalesce = opts.coalesceKey != null && opts.coalesceKey === state.lastCoalesceKey;
|
|
49
31
|
// Normalization can dissolve the node a caller wanted selected (e.g. the
|
|
50
32
|
// parent container of a removed child) — selection never points at a ghost.
|
|
@@ -66,16 +48,10 @@ function commit(state, root, opts = {}) {
|
|
|
66
48
|
* properties, not render behavior.
|
|
67
49
|
*/
|
|
68
50
|
export function builderReducer(state, action) {
|
|
69
|
-
const root =
|
|
51
|
+
const root = state.present;
|
|
70
52
|
switch (action.type) {
|
|
71
|
-
case 'select':
|
|
72
|
-
|
|
73
|
-
// that exist in the working graph; selection stays a pure-UI action.
|
|
74
|
-
const pageId = action.pageId !== undefined && state.present.pages.some((p) => p.id === action.pageId)
|
|
75
|
-
? action.pageId
|
|
76
|
-
: state.pageId;
|
|
77
|
-
return { ...state, selectedId: action.id, pageId };
|
|
78
|
-
}
|
|
53
|
+
case 'select':
|
|
54
|
+
return { ...state, selectedId: action.id };
|
|
79
55
|
case 'hover':
|
|
80
56
|
return state.hoveredId === action.id ? state : { ...state, hoveredId: action.id };
|
|
81
57
|
case 'viewport':
|
|
@@ -90,9 +66,7 @@ export function builderReducer(state, action) {
|
|
|
90
66
|
present: prev,
|
|
91
67
|
future: [state.present, ...state.future],
|
|
92
68
|
lastCoalesceKey: null,
|
|
93
|
-
selectedId: getNode(prev
|
|
94
|
-
? state.selectedId
|
|
95
|
-
: null,
|
|
69
|
+
selectedId: getNode(prev, state.selectedId ?? '') ? state.selectedId : null,
|
|
96
70
|
};
|
|
97
71
|
}
|
|
98
72
|
case 'redo': {
|
|
@@ -105,9 +79,7 @@ export function builderReducer(state, action) {
|
|
|
105
79
|
present: next,
|
|
106
80
|
future: state.future.slice(1),
|
|
107
81
|
lastCoalesceKey: null,
|
|
108
|
-
selectedId: getNode(next
|
|
109
|
-
? state.selectedId
|
|
110
|
-
: null,
|
|
82
|
+
selectedId: getNode(next, state.selectedId ?? '') ? state.selectedId : null,
|
|
111
83
|
};
|
|
112
84
|
}
|
|
113
85
|
case 'insert': {
|
|
@@ -161,116 +133,51 @@ export function builderReducer(state, action) {
|
|
|
161
133
|
});
|
|
162
134
|
case 'removeProp':
|
|
163
135
|
return commit(state, removeNodeProp(root, action.id, action.name));
|
|
164
|
-
case 'setCondition':
|
|
165
|
-
return commit(state, setNodeCondition(root, action.id, action.condition));
|
|
166
|
-
case 'setPageSeo': {
|
|
167
|
-
// Legacy page SEO (EC-171), in-memory only: update the working page's
|
|
168
|
-
// `seo` block. Undoable; selection unchanged; never persisted as a node
|
|
169
|
-
// op (page SEO lives on the page CMS document now).
|
|
170
|
-
const next = setWorkingPageSeo(state.present, action.pageId, action.seo);
|
|
171
|
-
if (next === state.present)
|
|
172
|
-
return state;
|
|
173
|
-
return {
|
|
174
|
-
...state,
|
|
175
|
-
past: [...state.past, state.present].slice(-HISTORY_LIMIT),
|
|
176
|
-
present: next,
|
|
177
|
-
future: [],
|
|
178
|
-
lastCoalesceKey: null,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
case 'loadGraph': {
|
|
182
|
-
// Replace the whole working graph (e.g. restoring a saved revision).
|
|
183
|
-
// `action.graph` is a CANONICAL layouts-only ProjectGraph (a restore from
|
|
184
|
-
// the append-only revision log); lift it into a WorkingGraph so the
|
|
185
|
-
// root-bearing editor has an editable page. Undoable, selection
|
|
186
|
-
// re-validated, normalized like the persisted graph.save it mirrors.
|
|
187
|
-
const graph = normalizeWorkingGraph(toWorkingGraph(action.graph));
|
|
188
|
-
const pageId = graph.pages.find((p) => p.id === state.pageId)
|
|
189
|
-
? state.pageId
|
|
190
|
-
: graph.pages[0].id;
|
|
191
|
-
const nextRoot = graph.pages.find((p) => p.id === pageId).root;
|
|
192
|
-
return {
|
|
193
|
-
...state,
|
|
194
|
-
past: [...state.past, state.present].slice(-HISTORY_LIMIT),
|
|
195
|
-
present: graph,
|
|
196
|
-
future: [],
|
|
197
|
-
pageId,
|
|
198
|
-
// Advance past the restored graph's ids too (EC-203), never backwards:
|
|
199
|
-
// a restore swaps in a fresh node namespace under the same counter.
|
|
200
|
-
nodeSeq: Math.max(state.nodeSeq, maxGraphNodeSeq(graph)),
|
|
201
|
-
lastCoalesceKey: null,
|
|
202
|
-
selectedId: getNode(nextRoot, state.selectedId ?? '') ? state.selectedId : nextRoot.id,
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
136
|
default:
|
|
206
137
|
return state;
|
|
207
138
|
}
|
|
208
139
|
}
|
|
209
140
|
const BuilderContext = createContext(null);
|
|
210
|
-
export function initialBuilderState(
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
// the persisted baseline agree from the first edit.
|
|
215
|
-
const working = 'pages' in graph ? graph : toWorkingGraph(graph);
|
|
216
|
-
const present = normalizeWorkingGraph(working);
|
|
141
|
+
export function initialBuilderState(composition) {
|
|
142
|
+
// Start container-normal (idempotent on healthy trees) so the session and the
|
|
143
|
+
// lifted baseline agree from the first edit.
|
|
144
|
+
const present = normalizeContainers(composition);
|
|
217
145
|
return {
|
|
218
146
|
past: [],
|
|
219
147
|
present,
|
|
220
148
|
future: [],
|
|
221
|
-
|
|
222
|
-
selectedId: present.pages[0].root.id,
|
|
149
|
+
selectedId: present.id,
|
|
223
150
|
hoveredId: null,
|
|
224
151
|
viewport: 'desktop',
|
|
225
|
-
// Seed past the ids the
|
|
226
|
-
//
|
|
152
|
+
// Seed past the ids the composition already carries (EC-203): a restored
|
|
153
|
+
// tree holds minted `node-<n>` ids, so starting at 0 would re-mint `node-1`
|
|
227
154
|
// on the first insert and produce a duplicate-node-id.
|
|
228
|
-
nodeSeq:
|
|
155
|
+
nodeSeq: maxNodeIdSeq(present),
|
|
229
156
|
lastCoalesceKey: null,
|
|
230
157
|
};
|
|
231
158
|
}
|
|
232
159
|
/**
|
|
233
|
-
* Undo/redo design (EC-141): the history stack
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
* (`graph.save`, message "Undo"/"Redo"). This is the honest mapping of a
|
|
240
|
-
* linear session history onto an append-only revision log: the session and
|
|
241
|
-
* the persisted latest revision always agree, and the persisted timeline
|
|
242
|
-
* records that an undo happened rather than pretending it didn't.
|
|
243
|
-
*
|
|
244
|
-
* Without a `projectId` (styleguide, isolated tests) the store is a pure
|
|
245
|
-
* in-memory editor and persists nothing.
|
|
160
|
+
* Undo/redo design (EC-141): the history stack is *UI state* — an in-memory
|
|
161
|
+
* stack of composition-root snapshots. EC-287 retired the DB-resident project
|
|
162
|
+
* graph and its append-only revision log, so the store no longer mirrors edits onto a
|
|
163
|
+
* persistence layer: it is a pure in-memory editor. The block-field editor lifts
|
|
164
|
+
* its committed composition into the open CMS document via `onChange`
|
|
165
|
+
* (`composition-field-editor`); the document's own Save persists it as content.
|
|
246
166
|
*/
|
|
247
|
-
export function BuilderProvider({
|
|
248
|
-
const [state, setState] = useState(() => initialBuilderState(
|
|
249
|
-
const [persistError, setPersistError] = useState(null);
|
|
167
|
+
export function BuilderProvider({ composition, children, }) {
|
|
168
|
+
const [state, setState] = useState(() => initialBuilderState(composition));
|
|
250
169
|
const stateRef = useRef(state);
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
projectId,
|
|
255
|
-
getOperations: async () => (await getStudioOperations()).operations,
|
|
256
|
-
onError: setPersistError,
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
// Plain-function dispatch (not `useReducer`) so each action sees the
|
|
260
|
-
// pre/post state synchronously — the persister derives the mirrored
|
|
261
|
-
// `graph.*` command from exactly that transition.
|
|
170
|
+
// Plain-function dispatch (not `useReducer`) so each action sees the pre/post
|
|
171
|
+
// state synchronously. EC-287: nothing persists a graph — committed edits are
|
|
172
|
+
// lifted into the CMS document by the composition-field editor's `onChange`.
|
|
262
173
|
const dispatch = useCallback((action) => {
|
|
263
|
-
const
|
|
264
|
-
const next = builderReducer(prev, action);
|
|
174
|
+
const next = builderReducer(stateRef.current, action);
|
|
265
175
|
stateRef.current = next;
|
|
266
176
|
setState(next);
|
|
267
|
-
persisterRef.current?.enqueue(action, { present: prev.present, pageId: prev.pageId, nodeSeq: prev.nodeSeq }, { present: next.present });
|
|
268
177
|
}, []);
|
|
269
|
-
const flushPersistence = useCallback(async () => persisterRef.current?.flush() ?? Promise.resolve(), []);
|
|
270
|
-
const clearPersistError = useCallback(() => setPersistError(null), []);
|
|
271
178
|
const store = useMemo(() => {
|
|
272
179
|
const selectedNode = state.selectedId
|
|
273
|
-
? (getNode(
|
|
180
|
+
? (getNode(state.present, state.selectedId) ?? null)
|
|
274
181
|
: null;
|
|
275
182
|
return {
|
|
276
183
|
...state,
|
|
@@ -278,11 +185,8 @@ export function BuilderProvider({ graph = validSampleGraph, workingGraph, projec
|
|
|
278
185
|
canRedo: state.future.length > 0,
|
|
279
186
|
selectedNode,
|
|
280
187
|
dispatch,
|
|
281
|
-
persistError,
|
|
282
|
-
clearPersistError,
|
|
283
|
-
flushPersistence,
|
|
284
188
|
};
|
|
285
|
-
}, [state, dispatch
|
|
189
|
+
}, [state, dispatch]);
|
|
286
190
|
return _jsx(BuilderContext.Provider, { value: store, children: children });
|
|
287
191
|
}
|
|
288
192
|
export function 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 { createContext, useCallback, useContext, useMemo, useRef, useState } 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 setNodeCondition,\n setNodeProp,\n validSampleGraph,\n} from '@elytracms/core/project-graph'\nimport type { ComponentNode, ProjectGraph } from '@elytracms/core/project-graph'\nimport { applyWrap } from '@elytracms/studio/lib/dnd/drop-rules'\nimport { createGraphPersister, getStudioOperations } from '@elytracms/studio/lib/operations'\nimport type { BuilderAction, GraphPersister, OperationError } from '@elytracms/studio/lib/operations'\nimport { normalizeWorkingGraph, setWorkingPageSeo, toWorkingGraph } from '@elytracms/studio/lib/working-graph'\nimport type { WorkingGraph } from '@elytracms/studio/lib/working-graph'\n\nconst HISTORY_LIMIT = 100\n\nexport type Viewport = 'desktop' | 'tablet' | 'mobile'\n\nexport interface BuilderState {\n past: WorkingGraph[]\n present: WorkingGraph\n future: WorkingGraph[]\n pageId: string\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\ntype Action = BuilderAction\n\nfunction activeRoot(state: BuilderState): ComponentNode {\n const page = state.present.pages.find((p) => p.id === state.pageId)\n return page!.root\n}\n\nfunction withRoot(graph: WorkingGraph, pageId: string, root: ComponentNode): WorkingGraph {\n return { ...graph, pages: graph.pages.map((p) => (p.id === pageId ? { ...p, root } : p)) }\n}\n\n/**\n * The highest minted-id sequence already present in the working graph (EC-203).\n * `nodeSeq` is a single session-wide counter feeding inserts on every page, so it\n * must clear the max across ALL pages' roots — never just the active one — or\n * switching pages and inserting would re-mint a collision on another page.\n */\nfunction maxGraphNodeSeq(graph: WorkingGraph): number {\n return graph.pages.reduce((max, page) => Math.max(max, maxNodeIdSeq(page.root)), 0)\n}\n\ninterface Counter {\n genId: () => string\n count: number\n}\nfunction counter(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)\n const present = withRoot(state.present, state.pageId, 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 = activeRoot(state)\n switch (action.type) {\n case 'select': {\n // Optional page switch (reference deep links, EC-172): only onto pages\n // that exist in the working graph; selection stays a pure-UI action.\n const pageId =\n action.pageId !== undefined && state.present.pages.some((p) => p.id === action.pageId)\n ? action.pageId\n : state.pageId\n return { ...state, selectedId: action.id, pageId }\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\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(\n prev.pages.find((p) => p.id === state.pageId)!.root,\n state.selectedId ?? '',\n )\n ? state.selectedId\n : 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(\n next.pages.find((p) => p.id === state.pageId)!.root,\n state.selectedId ?? '',\n )\n ? state.selectedId\n : null,\n }\n }\n\n case 'insert': {\n const c = counter(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 = counter(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 '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 = counter(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 const containerId = c.genId()\n const wrapped = applyWrap({\n root,\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 case 'setCondition':\n return commit(state, setNodeCondition(root, action.id, action.condition))\n case 'setPageSeo': {\n // Legacy page SEO (EC-171), in-memory only: update the working page's\n // `seo` block. Undoable; selection unchanged; never persisted as a node\n // op (page SEO lives on the page CMS document now).\n const next = setWorkingPageSeo(state.present, action.pageId, action.seo)\n if (next === state.present) return state\n return {\n ...state,\n past: [...state.past, state.present].slice(-HISTORY_LIMIT),\n present: next,\n future: [],\n lastCoalesceKey: null,\n }\n }\n case 'loadGraph': {\n // Replace the whole working graph (e.g. restoring a saved revision).\n // `action.graph` is a CANONICAL layouts-only ProjectGraph (a restore from\n // the append-only revision log); lift it into a WorkingGraph so the\n // root-bearing editor has an editable page. Undoable, selection\n // re-validated, normalized like the persisted graph.save it mirrors.\n const graph = normalizeWorkingGraph(toWorkingGraph(action.graph))\n const pageId = graph.pages.find((p) => p.id === state.pageId)\n ? state.pageId\n : graph.pages[0]!.id\n const nextRoot = graph.pages.find((p) => p.id === pageId)!.root\n return {\n ...state,\n past: [...state.past, state.present].slice(-HISTORY_LIMIT),\n present: graph,\n future: [],\n pageId,\n // Advance past the restored graph's ids too (EC-203), never backwards:\n // a restore swaps in a fresh node namespace under the same counter.\n nodeSeq: Math.max(state.nodeSeq, maxGraphNodeSeq(graph)),\n lastCoalesceKey: null,\n selectedId: getNode(nextRoot, state.selectedId ?? '') ? state.selectedId : nextRoot.id,\n }\n }\n default:\n return state\n }\n}\n\nexport interface BuilderStore extends BuilderState {\n canUndo: boolean\n canRedo: boolean\n selectedNode: ComponentNode | null\n dispatch: (action: Action) => void\n /**\n * Most recent failed/denied persisted mutation (structured, from the\n * operation result). Rendered by the shell — never a silent failure.\n */\n persistError: OperationError | null\n clearPersistError: () => void\n /** Await all queued persisted mutations (publish flows, tests). */\n flushPersistence: () => Promise<void>\n}\n\nconst BuilderContext = createContext<BuilderStore | null>(null)\n\nexport function initialBuilderState(graph: ProjectGraph | WorkingGraph): BuilderState {\n // Lift a canonical (layouts-only) ProjectGraph into a root-bearing\n // WorkingGraph; a WorkingGraph (synthetic composition surface) is used as-is.\n // Start container-normal (idempotent on healthy graphs) so the session and\n // the persisted baseline agree from the first edit.\n const working = 'pages' in graph ? graph : toWorkingGraph(graph)\n const present = normalizeWorkingGraph(working)\n return {\n past: [],\n present,\n future: [],\n pageId: present.pages[0]!.id,\n selectedId: present.pages[0]!.root.id,\n hoveredId: null,\n viewport: 'desktop',\n // Seed past the ids the hydrated graph already carries (EC-203): a restored\n // page 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: maxGraphNodeSeq(present),\n lastCoalesceKey: null,\n }\n}\n\n/**\n * Undo/redo design (EC-141): the history stack stays *UI state* — an\n * in-memory stack of working-graph snapshots, exactly as before. The graph\n * domain is append-only (load-latest → edit → save-revision), so a persisted\n * \"undo\" cannot rewind the timeline; instead every committed edit is mirrored\n * onto the operations layer as the matching `graph.*` command, and an\n * undo/redo persists the restored snapshot as a *new* revision\n * (`graph.save`, message \"Undo\"/\"Redo\"). This is the honest mapping of a\n * linear session history onto an append-only revision log: the session and\n * the persisted latest revision always agree, and the persisted timeline\n * records that an undo happened rather than pretending it didn't.\n *\n * Without a `projectId` (styleguide, isolated tests) the store is a pure\n * in-memory editor and persists nothing.\n */\nexport function BuilderProvider({\n graph = validSampleGraph,\n workingGraph,\n projectId,\n children,\n}: {\n /** Canonical (layouts-only) project graph; lifted into a working graph. */\n graph?: ProjectGraph\n /**\n * A pre-built working graph (the composition-field editor's synthetic page);\n * used as-is when present, overriding `graph`.\n */\n workingGraph?: WorkingGraph\n /** Enables persistence through the shared operations client. */\n projectId?: string\n children: ReactNode\n}) {\n const [state, setState] = useState<BuilderState>(() =>\n initialBuilderState(workingGraph ?? graph),\n )\n const [persistError, setPersistError] = useState<OperationError | null>(null)\n const stateRef = useRef(state)\n\n const persisterRef = useRef<GraphPersister | null>(null)\n if (persisterRef.current === null && projectId !== undefined) {\n persisterRef.current = createGraphPersister({\n projectId,\n getOperations: async () => (await getStudioOperations()).operations,\n onError: setPersistError,\n })\n }\n\n // Plain-function dispatch (not `useReducer`) so each action sees the\n // pre/post state synchronously — the persister derives the mirrored\n // `graph.*` command from exactly that transition.\n const dispatch = useCallback((action: Action) => {\n const prev = stateRef.current\n const next = builderReducer(prev, action)\n stateRef.current = next\n setState(next)\n persisterRef.current?.enqueue(\n action,\n { present: prev.present, pageId: prev.pageId, nodeSeq: prev.nodeSeq },\n { present: next.present },\n )\n }, [])\n\n const flushPersistence = useCallback(\n async () => persisterRef.current?.flush() ?? Promise.resolve(),\n [],\n )\n const clearPersistError = useCallback(() => setPersistError(null), [])\n\n const store = useMemo<BuilderStore>(() => {\n const selectedNode = state.selectedId\n ? (getNode(activeRoot(state), state.selectedId) ?? null)\n : null\n return {\n ...state,\n canUndo: state.past.length > 0,\n canRedo: state.future.length > 0,\n selectedNode,\n dispatch,\n persistError,\n clearPersistError,\n flushPersistence,\n }\n }, [state, dispatch, persistError, clearPersistError, flushPersistence])\n\n return <BuilderContext.Provider value={store}>{children}</BuilderContext.Provider>\n}\n\nexport function useBuilder(): BuilderStore {\n const store = useContext(BuilderContext)\n if (!store) throw new Error('useBuilder must be used within a <BuilderProvider>.')\n return store\n}\n"]}
|
|
1
|
+
{"version":3,"file":"builder-store.js","sourceRoot":"","sources":["../../../src/components/builder/builder-store.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEzF,OAAO,EACL,eAAe,EACf,aAAa,EACb,OAAO,EACP,aAAa,EACb,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,mBAAmB,EACnB,UAAU,EACV,cAAc,EACd,WAAW,EACX,WAAW,GACZ,MAAM,+BAA+B,CAAA;AAEtC,OAAO,EAAE,SAAS,EAAE,MAAM,sCAAsC,CAAA;AAEhE,MAAM,aAAa,GAAG,GAAG,CAAA;AA2DzB,SAAS,OAAO,CAAC,KAAa;IAC5B,MAAM,CAAC,GAAY;QACjB,KAAK,EAAE,KAAK;QACZ,KAAK,EAAE,GAAG,EAAE;YACV,CAAC,CAAC,KAAK,IAAI,CAAC,CAAA;YACZ,OAAO,QAAQ,CAAC,CAAC,KAAK,EAAE,CAAA;QAC1B,CAAC;KACF,CAAA;IACD,OAAO,CAAC,CAAA;AACV,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,MAAM,CACb,KAAmB,EACnB,IAAmB,EACnB,OAA+E,EAAE;IAEjF,MAAM,UAAU,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAA;IAC5C,MAAM,OAAO,GAAG,UAAU,CAAA;IAC1B,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,IAAI,IAAI,IAAI,IAAI,CAAC,WAAW,KAAK,KAAK,CAAC,eAAe,CAAA;IACvF,yEAAyE;IACzE,4EAA4E;IAC5E,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAA;IAClF,MAAM,UAAU,GAAG,OAAO,KAAK,IAAI,IAAI,OAAO,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAA;IACpF,OAAO;QACL,GAAG,KAAK;QACR,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,aAAa,CAAC;QAClF,OAAO;QACP,MAAM,EAAE,EAAE;QACV,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO;QACtC,eAAe,EAAE,IAAI,CAAC,WAAW,IAAI,IAAI;QACzC,UAAU;KACX,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,KAAmB,EAAE,MAAc;IAChE,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAA;IAC1B,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,QAAQ;YACX,OAAO,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,EAAE,CAAA;QAC5C,KAAK,OAAO;YACV,OAAO,KAAK,CAAC,SAAS,KAAK,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,EAAE,CAAA;QACnF,KAAK,UAAU;YACb,OAAO,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAA;QAEhD,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;YAC9B,IAAI,CAAC,IAAI;gBAAE,OAAO,KAAK,CAAA;YACvB,OAAO;gBACL,GAAG,KAAK;gBACR,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;gBAC7B,OAAO,EAAE,IAAI;gBACb,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC;gBACxC,eAAe,EAAE,IAAI;gBACrB,UAAU,EAAE,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI;aAC5E,CAAA;QACH,CAAC;QACD,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;YAC5B,IAAI,CAAC,IAAI;gBAAE,OAAO,KAAK,CAAA;YACvB,OAAO;gBACL,GAAG,KAAK;gBACR,IAAI,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC;gBACpC,OAAO,EAAE,IAAI;gBACb,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;gBAC7B,eAAe,EAAE,IAAI;gBACrB,UAAU,EAAE,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI;aAC5E,CAAA;QACH,CAAC;QAED,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,MAAM,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;YAChC,MAAM,IAAI,GAAG,eAAe,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAA;YAClD,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;YAClD,OAAO,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE;gBACzB,OAAO,EAAE,CAAC,CAAC,KAAK;gBAChB,UAAU,EAAE,MAAM,CAAC,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE;aACjE,CAAC,CAAA;QACJ,CAAC;QACD,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,CAAA;YAC7C,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,CAAA;YACxC,MAAM,UAAU,GACd,KAAK,CAAC,UAAU,KAAK,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAA;YACjF,OAAO,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,UAAU,EAAE,CAAC,CAAA;QAC5C,CAAC;QACD,KAAK,WAAW,CAAC,CAAC,CAAC;YACjB,MAAM,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;YAChC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAA;YACrE,OAAO,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,IAAI,KAAK,CAAC,UAAU,EAAE,CAAC,CAAA;QACzF,CAAC;QACD,KAAK,SAAS;YACZ,OAAO,MAAM,CAAC,KAAK,EAAE,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;QAClE,KAAK,MAAM;YACT,OAAO,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAA;QAChE,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,wEAAwE;YACxE,qEAAqE;YACrE,sEAAsE;YACtE,iBAAiB;YACjB,MAAM,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;YAChC,MAAM,OAAO,GACX,MAAM,CAAC,OAAO,CAAC,IAAI,KAAK,MAAM;gBAC5B,CAAC,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE;gBAC1D,CAAC,CAAC,EAAE,IAAI,EAAE,SAAkB,EAAE,IAAI,EAAE,eAAe,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,EAAE,CAAA;YACvF,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,EAAE,CAAA;YAC7B,MAAM,OAAO,GAAG,SAAS,CAAC;gBACxB,IAAI;gBACJ,WAAW,EAAE,MAAM,CAAC,WAAW;gBAC/B,IAAI,EAAE,MAAM,CAAC,IAAI;gBACjB,OAAO;gBACP,WAAW;aACZ,CAAC,CAAA;YACF,IAAI,CAAC,OAAO;gBAAE,OAAO,KAAK,CAAA;YAC1B,OAAO,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC,CAAA;QACnF,CAAC;QACD,KAAK,SAAS;YACZ,OAAO,MAAM,CAAC,KAAK,EAAE,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE;gBAC5E,WAAW,EAAE,MAAM,CAAC,WAAW;aAChC,CAAC,CAAA;QACJ,KAAK,YAAY;YACf,OAAO,MAAM,CAAC,KAAK,EAAE,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;QACpE;YACE,OAAO,KAAK,CAAA;IAChB,CAAC;AACH,CAAC;AASD,MAAM,cAAc,GAAG,aAAa,CAAsB,IAAI,CAAC,CAAA;AAE/D,MAAM,UAAU,mBAAmB,CAAC,WAA0B;IAC5D,8EAA8E;IAC9E,6CAA6C;IAC7C,MAAM,OAAO,GAAG,mBAAmB,CAAC,WAAW,CAAC,CAAA;IAChD,OAAO;QACL,IAAI,EAAE,EAAE;QACR,OAAO;QACP,MAAM,EAAE,EAAE;QACV,UAAU,EAAE,OAAO,CAAC,EAAE;QACtB,SAAS,EAAE,IAAI;QACf,QAAQ,EAAE,SAAS;QACnB,yEAAyE;QACzE,4EAA4E;QAC5E,uDAAuD;QACvD,OAAO,EAAE,YAAY,CAAC,OAAO,CAAC;QAC9B,eAAe,EAAE,IAAI;KACtB,CAAA;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,EAC9B,WAAW,EACX,QAAQ,GAKT;IACC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAe,GAAG,EAAE,CAAC,mBAAmB,CAAC,WAAW,CAAC,CAAC,CAAA;IACxF,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IAE9B,8EAA8E;IAC9E,8EAA8E;IAC9E,6EAA6E;IAC7E,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,MAAc,EAAE,EAAE;QAC9C,MAAM,IAAI,GAAG,cAAc,CAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;QACrD,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAA;QACvB,QAAQ,CAAC,IAAI,CAAC,CAAA;IAChB,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,KAAK,GAAG,OAAO,CAAe,GAAG,EAAE;QACvC,MAAM,YAAY,GAAG,KAAK,CAAC,UAAU;YACnC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC;YACpD,CAAC,CAAC,IAAI,CAAA;QACR,OAAO;YACL,GAAG,KAAK;YACR,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC;YAC9B,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC;YAChC,YAAY;YACZ,QAAQ;SACT,CAAA;IACH,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAA;IAErB,OAAO,KAAC,cAAc,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAA2B,CAAA;AACpF,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,MAAM,KAAK,GAAG,UAAU,CAAC,cAAc,CAAC,CAAA;IACxC,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAA;IAClF,OAAO,KAAK,CAAA;AACd,CAAC","sourcesContent":["import { createContext, useCallback, useContext, useMemo, useRef, useState } 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 setNodeProp,\n} from '@elytracms/core/project-graph'\nimport type { ComponentNode, InsertTarget, PropValue } from '@elytracms/core/project-graph'\nimport { applyWrap } from '@elytracms/studio/lib/dnd/drop-rules'\n\nconst HISTORY_LIMIT = 100\n\nexport type Viewport = 'desktop' | 'tablet' | 'mobile'\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-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\ntype Action = BuilderAction\n\ninterface Counter {\n genId: () => string\n count: number\n}\nfunction counter(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)\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 = counter(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 = counter(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 '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 = counter(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 const containerId = c.genId()\n const wrapped = applyWrap({\n root,\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\nexport interface BuilderStore extends BuilderState {\n canUndo: boolean\n canRedo: boolean\n selectedNode: ComponentNode | null\n dispatch: (action: Action) => void\n}\n\nconst BuilderContext = createContext<BuilderStore | null>(null)\n\nexport function initialBuilderState(composition: ComponentNode): 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)\n return {\n past: [],\n present,\n future: [],\n selectedId: present.id,\n hoveredId: null,\n viewport: 'desktop',\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 }\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 children,\n}: {\n /** The composition root to edit (the composition-field editor's synthetic root). */\n composition: ComponentNode\n children: ReactNode\n}) {\n const [state, setState] = useState<BuilderState>(() => initialBuilderState(composition))\n const stateRef = useRef(state)\n\n // Plain-function dispatch (not `useReducer`) so each action sees the pre/post\n // state synchronously. EC-287: nothing persists a graph — committed edits are\n // lifted into the CMS document by the composition-field editor's `onChange`.\n const dispatch = useCallback((action: Action) => {\n const next = builderReducer(stateRef.current, action)\n stateRef.current = next\n setState(next)\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 dispatch,\n }\n }, [state, dispatch])\n\n return <BuilderContext.Provider value={store}>{children}</BuilderContext.Provider>\n}\n\nexport function useBuilder(): BuilderStore {\n const store = useContext(BuilderContext)\n if (!store) throw new Error('useBuilder must be used within a <BuilderProvider>.')\n return store\n}\n"]}
|