@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.
Files changed (93) hide show
  1. package/convex/cms.ts +8 -31
  2. package/convex/content.ts +3 -44
  3. package/convex/delivery.ts +8 -42
  4. package/convex/functions.ts +17 -2
  5. package/convex/members.ts +2 -2
  6. package/convex/referenceMaintenance.ts +4 -73
  7. package/convex/references.ts +4 -2
  8. package/convex/schema.ts +17 -37
  9. package/convex/sync.ts +10 -42
  10. package/convex/triggers.ts +57 -0
  11. package/convex/webhooks.ts +3 -2
  12. package/dist/components/builder/builder-store.d.ts +85 -39
  13. package/dist/components/builder/builder-store.js +29 -125
  14. package/dist/components/builder/builder-store.js.map +1 -1
  15. package/dist/components/builder/canvas.js +23 -55
  16. package/dist/components/builder/canvas.js.map +1 -1
  17. package/dist/components/builder/clipboard-actions.js +3 -3
  18. package/dist/components/builder/clipboard-actions.js.map +1 -1
  19. package/dist/components/builder/composition-field-editor.js +13 -23
  20. package/dist/components/builder/composition-field-editor.js.map +1 -1
  21. package/dist/components/builder/insert-context.js +1 -1
  22. package/dist/components/builder/insert-context.js.map +1 -1
  23. package/dist/components/builder/page-tree.js +3 -4
  24. package/dist/components/builder/page-tree.js.map +1 -1
  25. package/dist/components/builder/property-inspector.js +4 -64
  26. package/dist/components/builder/property-inspector.js.map +1 -1
  27. package/dist/components/cms/cell.d.ts +0 -4
  28. package/dist/components/cms/cell.js +1 -5
  29. package/dist/components/cms/cell.js.map +1 -1
  30. package/dist/components/cms/cms-store.d.ts +1 -1
  31. package/dist/components/cms/cms-store.js +4 -9
  32. package/dist/components/cms/cms-store.js.map +1 -1
  33. package/dist/components/cms/collection-table.js +1 -1
  34. package/dist/components/cms/collection-table.js.map +1 -1
  35. package/dist/components/cms/detail-editor.js +2 -2
  36. package/dist/components/cms/detail-editor.js.map +1 -1
  37. package/dist/components/cms/doc-state.d.ts +0 -5
  38. package/dist/components/cms/doc-state.js +1 -7
  39. package/dist/components/cms/doc-state.js.map +1 -1
  40. package/dist/components/cms/editable-cell.js +1 -1
  41. package/dist/components/cms/editable-cell.js.map +1 -1
  42. package/dist/components/collab/collab-context.d.ts +6 -14
  43. package/dist/components/collab/collab-context.js +3 -12
  44. package/dist/components/collab/collab-context.js.map +1 -1
  45. package/dist/components/collab/collab-seed.d.ts +7 -19
  46. package/dist/components/collab/collab-seed.js +9 -31
  47. package/dist/components/collab/collab-seed.js.map +1 -1
  48. package/dist/components/components/registry-browser.js +6 -12
  49. package/dist/components/components/registry-browser.js.map +1 -1
  50. package/dist/components/guards.d.ts +0 -9
  51. package/dist/components/guards.js +0 -12
  52. package/dist/components/guards.js.map +1 -1
  53. package/dist/components/section-state.d.ts +0 -2
  54. package/dist/components/section-state.js +0 -9
  55. package/dist/components/section-state.js.map +1 -1
  56. package/dist/components/section-surface.d.ts +3 -7
  57. package/dist/components/section-surface.js +5 -12
  58. package/dist/components/section-surface.js.map +1 -1
  59. package/dist/components/sections.d.ts +0 -1
  60. package/dist/components/sections.js +0 -10
  61. package/dist/components/sections.js.map +1 -1
  62. package/dist/components/studio-project-screen.js +13 -14
  63. package/dist/components/studio-project-screen.js.map +1 -1
  64. package/dist/components/studio-shell.d.ts +0 -4
  65. package/dist/components/studio-shell.js +1 -5
  66. package/dist/components/studio-shell.js.map +1 -1
  67. package/dist/lib/operations/graph-commands.d.ts +2 -6
  68. package/dist/lib/operations/graph-commands.js +1 -2
  69. package/dist/lib/operations/graph-commands.js.map +1 -1
  70. package/dist/lib/operations/index.d.ts +0 -2
  71. package/dist/lib/operations/index.js +0 -2
  72. package/dist/lib/operations/index.js.map +1 -1
  73. package/dist/lib/operations/session-hydration.d.ts +14 -24
  74. package/dist/lib/operations/session-hydration.js +15 -34
  75. package/dist/lib/operations/session-hydration.js.map +1 -1
  76. package/dist/lib/persistence/convex-adapter.d.ts +1 -3
  77. package/dist/lib/persistence/convex-adapter.js +1 -154
  78. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  79. package/dist/lib/persona/labels.js +0 -2
  80. package/dist/lib/persona/labels.js.map +1 -1
  81. package/dist/lib/references/reference-model.d.ts +0 -16
  82. package/dist/lib/references/reference-model.js +2 -54
  83. package/dist/lib/references/reference-model.js.map +1 -1
  84. package/dist/lib/references/references-store.js +3 -4
  85. package/dist/lib/references/references-store.js.map +1 -1
  86. package/dist/lib/registry/index.d.ts +0 -16
  87. package/dist/lib/registry/index.js +0 -15
  88. package/dist/lib/registry/index.js.map +1 -1
  89. package/dist/lib/webhooks/tags.js +2 -16
  90. package/dist/lib/webhooks/tags.js.map +1 -1
  91. package/package.json +3 -2
  92. package/convex/graphs.ts +0 -55
  93. 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
+ })
@@ -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: `publishing:publish` / `publishing:unpublish`
11
- * schedule it via `ctx.scheduler.runAfter(0, internal.webhooks.deliver, …)`
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, ProjectGraph } from '@elytracms/core/project-graph';
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
- past: WorkingGraph[];
8
- present: WorkingGraph;
9
- future: WorkingGraph[];
10
- pageId: string;
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(graph: ProjectGraph | WorkingGraph): BuilderState;
100
+ export declare function initialBuilderState(composition: ComponentNode): BuilderState;
41
101
  /**
42
- * Undo/redo design (EC-141): the history stack stays *UI state* — an
43
- * in-memory stack of working-graph snapshots, exactly as before. The graph
44
- * domain is append-only (load-latest → edit → save-revision), so a persisted
45
- * "undo" cannot rewind the timeline; instead every committed edit is mirrored
46
- * onto the operations layer as the matching `graph.*` command, and an
47
- * undo/redo persists the restored snapshot as a *new* revision
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({ graph, workingGraph, projectId, children, }: {
57
- /** Canonical (layouts-only) project graph; lifted into a working graph. */
58
- graph?: ProjectGraph;
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, setNodeCondition, setNodeProp, validSampleGraph, } from '@elytracms/core/project-graph';
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 = withRoot(state.present, state.pageId, normalized);
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 = activeRoot(state);
51
+ const root = state.present;
70
52
  switch (action.type) {
71
- case 'select': {
72
- // Optional page switch (reference deep links, EC-172): only onto pages
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.pages.find((p) => p.id === state.pageId).root, state.selectedId ?? '')
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.pages.find((p) => p.id === state.pageId).root, state.selectedId ?? '')
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(graph) {
211
- // Lift a canonical (layouts-only) ProjectGraph into a root-bearing
212
- // WorkingGraph; a WorkingGraph (synthetic composition surface) is used as-is.
213
- // Start container-normal (idempotent on healthy graphs) so the session and
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
- pageId: present.pages[0].id,
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 hydrated graph already carries (EC-203): a restored
226
- // page holds minted `node-<n>` ids, so starting at 0 would re-mint `node-1`
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: maxGraphNodeSeq(present),
155
+ nodeSeq: maxNodeIdSeq(present),
229
156
  lastCoalesceKey: null,
230
157
  };
231
158
  }
232
159
  /**
233
- * Undo/redo design (EC-141): the history stack stays *UI state* — an
234
- * in-memory stack of working-graph snapshots, exactly as before. The graph
235
- * domain is append-only (load-latest → edit → save-revision), so a persisted
236
- * "undo" cannot rewind the timeline; instead every committed edit is mirrored
237
- * onto the operations layer as the matching `graph.*` command, and an
238
- * undo/redo persists the restored snapshot as a *new* revision
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({ graph = validSampleGraph, workingGraph, projectId, children, }) {
248
- const [state, setState] = useState(() => initialBuilderState(workingGraph ?? graph));
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
- const persisterRef = useRef(null);
252
- if (persisterRef.current === null && projectId !== undefined) {
253
- persisterRef.current = createGraphPersister({
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 prev = stateRef.current;
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(activeRoot(state), state.selectedId) ?? null)
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, persistError, clearPersistError, flushPersistence]);
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":";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,gBAAgB,EAChB,WAAW,EACX,gBAAgB,GACjB,MAAM,+BAA+B,CAAA;AAEtC,OAAO,EAAE,SAAS,EAAE,MAAM,sCAAsC,CAAA;AAChE,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAA;AAE5F,OAAO,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAA;AAG9G,MAAM,aAAa,GAAG,GAAG,CAAA;AAoBzB,SAAS,UAAU,CAAC,KAAmB;IACrC,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,KAAK,CAAC,MAAM,CAAC,CAAA;IACnE,OAAO,IAAK,CAAC,IAAI,CAAA;AACnB,CAAC;AAED,SAAS,QAAQ,CAAC,KAAmB,EAAE,MAAc,EAAE,IAAmB;IACxE,OAAO,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;AAC5F,CAAC;AAED;;;;;GAKG;AACH,SAAS,eAAe,CAAC,KAAmB;IAC1C,OAAO,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;AACrF,CAAC;AAMD,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,QAAQ,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;IACjE,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,UAAU,CAAC,KAAK,CAAC,CAAA;IAC9B,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,uEAAuE;YACvE,qEAAqE;YACrE,MAAM,MAAM,GACV,MAAM,CAAC,MAAM,KAAK,SAAS,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,MAAM,CAAC,MAAM,CAAC;gBACpF,CAAC,CAAC,MAAM,CAAC,MAAM;gBACf,CAAC,CAAC,KAAK,CAAC,MAAM,CAAA;YAClB,OAAO,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,CAAA;QACpD,CAAC;QACD,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,CACjB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,KAAK,CAAC,MAAM,CAAE,CAAC,IAAI,EACnD,KAAK,CAAC,UAAU,IAAI,EAAE,CACvB;oBACC,CAAC,CAAC,KAAK,CAAC,UAAU;oBAClB,CAAC,CAAC,IAAI;aACT,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,CACjB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,KAAK,CAAC,MAAM,CAAE,CAAC,IAAI,EACnD,KAAK,CAAC,UAAU,IAAI,EAAE,CACvB;oBACC,CAAC,CAAC,KAAK,CAAC,UAAU;oBAClB,CAAC,CAAC,IAAI;aACT,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,KAAK,cAAc;YACjB,OAAO,MAAM,CAAC,KAAK,EAAE,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,CAAA;QAC3E,KAAK,YAAY,CAAC,CAAC,CAAC;YAClB,sEAAsE;YACtE,wEAAwE;YACxE,oDAAoD;YACpD,MAAM,IAAI,GAAG,iBAAiB,CAAC,KAAK,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAAA;YACxE,IAAI,IAAI,KAAK,KAAK,CAAC,OAAO;gBAAE,OAAO,KAAK,CAAA;YACxC,OAAO;gBACL,GAAG,KAAK;gBACR,IAAI,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,aAAa,CAAC;gBAC1D,OAAO,EAAE,IAAI;gBACb,MAAM,EAAE,EAAE;gBACV,eAAe,EAAE,IAAI;aACtB,CAAA;QACH,CAAC;QACD,KAAK,WAAW,CAAC,CAAC,CAAC;YACjB,qEAAqE;YACrE,0EAA0E;YAC1E,oEAAoE;YACpE,gEAAgE;YAChE,qEAAqE;YACrE,MAAM,KAAK,GAAG,qBAAqB,CAAC,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;YACjE,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,KAAK,CAAC,MAAM,CAAC;gBAC3D,CAAC,CAAC,KAAK,CAAC,MAAM;gBACd,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,EAAE,CAAA;YACtB,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,MAAM,CAAE,CAAC,IAAI,CAAA;YAC/D,OAAO;gBACL,GAAG,KAAK;gBACR,IAAI,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,aAAa,CAAC;gBAC1D,OAAO,EAAE,KAAK;gBACd,MAAM,EAAE,EAAE;gBACV,MAAM;gBACN,uEAAuE;gBACvE,oEAAoE;gBACpE,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,eAAe,CAAC,KAAK,CAAC,CAAC;gBACxD,eAAe,EAAE,IAAI;gBACrB,UAAU,EAAE,OAAO,CAAC,QAAQ,EAAE,KAAK,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE;aACvF,CAAA;QACH,CAAC;QACD;YACE,OAAO,KAAK,CAAA;IAChB,CAAC;AACH,CAAC;AAiBD,MAAM,cAAc,GAAG,aAAa,CAAsB,IAAI,CAAC,CAAA;AAE/D,MAAM,UAAU,mBAAmB,CAAC,KAAkC;IACpE,mEAAmE;IACnE,8EAA8E;IAC9E,2EAA2E;IAC3E,oDAAoD;IACpD,MAAM,OAAO,GAAG,OAAO,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,cAAc,CAAC,KAAK,CAAC,CAAA;IAChE,MAAM,OAAO,GAAG,qBAAqB,CAAC,OAAO,CAAC,CAAA;IAC9C,OAAO;QACL,IAAI,EAAE,EAAE;QACR,OAAO;QACP,MAAM,EAAE,EAAE;QACV,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,EAAE;QAC5B,UAAU,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,IAAI,CAAC,EAAE;QACrC,SAAS,EAAE,IAAI;QACf,QAAQ,EAAE,SAAS;QACnB,4EAA4E;QAC5E,4EAA4E;QAC5E,uDAAuD;QACvD,OAAO,EAAE,eAAe,CAAC,OAAO,CAAC;QACjC,eAAe,EAAE,IAAI;KACtB,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,eAAe,CAAC,EAC9B,KAAK,GAAG,gBAAgB,EACxB,YAAY,EACZ,SAAS,EACT,QAAQ,GAYT;IACC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAe,GAAG,EAAE,CACpD,mBAAmB,CAAC,YAAY,IAAI,KAAK,CAAC,CAC3C,CAAA;IACD,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,QAAQ,CAAwB,IAAI,CAAC,CAAA;IAC7E,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IAE9B,MAAM,YAAY,GAAG,MAAM,CAAwB,IAAI,CAAC,CAAA;IACxD,IAAI,YAAY,CAAC,OAAO,KAAK,IAAI,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC7D,YAAY,CAAC,OAAO,GAAG,oBAAoB,CAAC;YAC1C,SAAS;YACT,aAAa,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,mBAAmB,EAAE,CAAC,CAAC,UAAU;YACnE,OAAO,EAAE,eAAe;SACzB,CAAC,CAAA;IACJ,CAAC;IAED,qEAAqE;IACrE,oEAAoE;IACpE,kDAAkD;IAClD,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,MAAc,EAAE,EAAE;QAC9C,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAA;QAC7B,MAAM,IAAI,GAAG,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;QACzC,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAA;QACvB,QAAQ,CAAC,IAAI,CAAC,CAAA;QACd,YAAY,CAAC,OAAO,EAAE,OAAO,CAC3B,MAAM,EACN,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,EACrE,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAC1B,CAAA;IACH,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,gBAAgB,GAAG,WAAW,CAClC,KAAK,IAAI,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,OAAO,CAAC,OAAO,EAAE,EAC9D,EAAE,CACH,CAAA;IACD,MAAM,iBAAiB,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAA;IAEtE,MAAM,KAAK,GAAG,OAAO,CAAe,GAAG,EAAE;QACvC,MAAM,YAAY,GAAG,KAAK,CAAC,UAAU;YACnC,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC;YACxD,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;YACR,YAAY;YACZ,iBAAiB;YACjB,gBAAgB;SACjB,CAAA;IACH,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,CAAC,CAAC,CAAA;IAExE,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 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":"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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 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"]}