@elytracms/studio 0.0.14 → 0.0.16

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 (143) 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/app-views/default-modules.d.ts +41 -0
  13. package/dist/components/app-views/default-modules.js +201 -0
  14. package/dist/components/app-views/default-modules.js.map +1 -0
  15. package/dist/components/app-views/module.d.ts +93 -0
  16. package/dist/components/app-views/module.js +75 -0
  17. package/dist/components/app-views/module.js.map +1 -0
  18. package/dist/components/app-views/sidebar.d.ts +95 -0
  19. package/dist/components/app-views/sidebar.js +153 -0
  20. package/dist/components/app-views/sidebar.js.map +1 -0
  21. package/dist/components/builder/builder-store.d.ts +92 -40
  22. package/dist/components/builder/builder-store.js +30 -126
  23. package/dist/components/builder/builder-store.js.map +1 -1
  24. package/dist/components/builder/canvas-breakpoints.d.ts +53 -0
  25. package/dist/components/builder/canvas-breakpoints.js +55 -0
  26. package/dist/components/builder/canvas-breakpoints.js.map +1 -0
  27. package/dist/components/builder/canvas.js +35 -61
  28. package/dist/components/builder/canvas.js.map +1 -1
  29. package/dist/components/builder/clipboard-actions.js +3 -3
  30. package/dist/components/builder/clipboard-actions.js.map +1 -1
  31. package/dist/components/builder/composition-field-editor.js +31 -40
  32. package/dist/components/builder/composition-field-editor.js.map +1 -1
  33. package/dist/components/builder/insert-context.js +1 -1
  34. package/dist/components/builder/insert-context.js.map +1 -1
  35. package/dist/components/builder/page-tree.js +3 -4
  36. package/dist/components/builder/page-tree.js.map +1 -1
  37. package/dist/components/builder/property-inspector.js +4 -64
  38. package/dist/components/builder/property-inspector.js.map +1 -1
  39. package/dist/components/cms/bulk-actions.d.ts +23 -5
  40. package/dist/components/cms/bulk-actions.js +7 -3
  41. package/dist/components/cms/bulk-actions.js.map +1 -1
  42. package/dist/components/cms/bulk-bar.d.ts +22 -6
  43. package/dist/components/cms/bulk-bar.js +46 -9
  44. package/dist/components/cms/bulk-bar.js.map +1 -1
  45. package/dist/components/cms/cell.d.ts +0 -4
  46. package/dist/components/cms/cell.js +1 -5
  47. package/dist/components/cms/cell.js.map +1 -1
  48. package/dist/components/cms/cms-index.d.ts +4 -2
  49. package/dist/components/cms/cms-index.js +6 -4
  50. package/dist/components/cms/cms-index.js.map +1 -1
  51. package/dist/components/cms/cms-store.d.ts +7 -6
  52. package/dist/components/cms/cms-store.js +55 -18
  53. package/dist/components/cms/cms-store.js.map +1 -1
  54. package/dist/components/cms/collection-table.js +8 -4
  55. package/dist/components/cms/collection-table.js.map +1 -1
  56. package/dist/components/cms/collections-sidebar.d.ts +10 -1
  57. package/dist/components/cms/collections-sidebar.js +13 -2
  58. package/dist/components/cms/collections-sidebar.js.map +1 -1
  59. package/dist/components/cms/detail-editor.js +2 -2
  60. package/dist/components/cms/detail-editor.js.map +1 -1
  61. package/dist/components/cms/doc-state.d.ts +0 -5
  62. package/dist/components/cms/doc-state.js +1 -7
  63. package/dist/components/cms/doc-state.js.map +1 -1
  64. package/dist/components/cms/editable-cell.js +1 -1
  65. package/dist/components/cms/editable-cell.js.map +1 -1
  66. package/dist/components/cms/field-control.d.ts +11 -2
  67. package/dist/components/cms/field-control.js +22 -1
  68. package/dist/components/cms/field-control.js.map +1 -1
  69. package/dist/components/cms/field-renderer.d.ts +71 -0
  70. package/dist/components/cms/field-renderer.js +67 -0
  71. package/dist/components/cms/field-renderer.js.map +1 -0
  72. package/dist/components/cms/filter-builder.js +6 -28
  73. package/dist/components/cms/filter-builder.js.map +1 -1
  74. package/dist/components/cms/scalar-value-editor.d.ts +35 -0
  75. package/dist/components/cms/scalar-value-editor.js +43 -0
  76. package/dist/components/cms/scalar-value-editor.js.map +1 -0
  77. package/dist/components/collab/collab-context.d.ts +6 -14
  78. package/dist/components/collab/collab-context.js +3 -12
  79. package/dist/components/collab/collab-context.js.map +1 -1
  80. package/dist/components/collab/collab-seed.d.ts +7 -19
  81. package/dist/components/collab/collab-seed.js +9 -31
  82. package/dist/components/collab/collab-seed.js.map +1 -1
  83. package/dist/components/components/registry-browser.js +6 -12
  84. package/dist/components/components/registry-browser.js.map +1 -1
  85. package/dist/components/guards.d.ts +0 -9
  86. package/dist/components/guards.js +0 -12
  87. package/dist/components/guards.js.map +1 -1
  88. package/dist/components/project-session.js +8 -3
  89. package/dist/components/project-session.js.map +1 -1
  90. package/dist/components/section-nav.d.ts +10 -10
  91. package/dist/components/section-nav.js +48 -25
  92. package/dist/components/section-nav.js.map +1 -1
  93. package/dist/components/section-state.d.ts +0 -2
  94. package/dist/components/section-state.js +0 -9
  95. package/dist/components/section-state.js.map +1 -1
  96. package/dist/components/section-surface.d.ts +3 -7
  97. package/dist/components/section-surface.js +6 -14
  98. package/dist/components/section-surface.js.map +1 -1
  99. package/dist/components/sections.d.ts +0 -1
  100. package/dist/components/sections.js +0 -10
  101. package/dist/components/sections.js.map +1 -1
  102. package/dist/components/studio-project-screen.d.ts +3 -4
  103. package/dist/components/studio-project-screen.js +41 -160
  104. package/dist/components/studio-project-screen.js.map +1 -1
  105. package/dist/components/studio-shell.d.ts +0 -4
  106. package/dist/components/studio-shell.js +14 -14
  107. package/dist/components/studio-shell.js.map +1 -1
  108. package/dist/lib/data/collection-content.d.ts +19 -1
  109. package/dist/lib/data/collection-content.js +16 -0
  110. package/dist/lib/data/collection-content.js.map +1 -1
  111. package/dist/lib/operations/graph-commands.d.ts +2 -6
  112. package/dist/lib/operations/graph-commands.js +1 -2
  113. package/dist/lib/operations/graph-commands.js.map +1 -1
  114. package/dist/lib/operations/index.d.ts +0 -2
  115. package/dist/lib/operations/index.js +0 -2
  116. package/dist/lib/operations/index.js.map +1 -1
  117. package/dist/lib/operations/session-hydration.d.ts +14 -24
  118. package/dist/lib/operations/session-hydration.js +15 -34
  119. package/dist/lib/operations/session-hydration.js.map +1 -1
  120. package/dist/lib/persistence/convex-adapter.d.ts +1 -3
  121. package/dist/lib/persistence/convex-adapter.js +1 -154
  122. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  123. package/dist/lib/persona/labels.js +0 -2
  124. package/dist/lib/persona/labels.js.map +1 -1
  125. package/dist/lib/references/reference-model.d.ts +0 -16
  126. package/dist/lib/references/reference-model.js +2 -54
  127. package/dist/lib/references/reference-model.js.map +1 -1
  128. package/dist/lib/references/references-store.js +3 -4
  129. package/dist/lib/references/references-store.js.map +1 -1
  130. package/dist/lib/registry/index.d.ts +0 -16
  131. package/dist/lib/registry/index.js +0 -15
  132. package/dist/lib/registry/index.js.map +1 -1
  133. package/dist/lib/webhooks/tags.js +2 -16
  134. package/dist/lib/webhooks/tags.js.map +1 -1
  135. package/dist/lib/workspace/index.d.ts +27 -1
  136. package/dist/lib/workspace/index.js +34 -0
  137. package/dist/lib/workspace/index.js.map +1 -1
  138. package/dist/router/match.d.ts +22 -34
  139. package/dist/router/match.js +22 -40
  140. package/dist/router/match.js.map +1 -1
  141. package/package.json +3 -2
  142. package/convex/graphs.ts +0 -55
  143. package/convex/publishing.ts +0 -76
@@ -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
- 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`
151
+ viewport: 'full',
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":"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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":"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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\n/**\n * A canvas preview-width key (EC-250): `'full'` (unconstrained) or a configured\n * breakpoint name (`'sm'`, `'md'`, …). Dynamic because the set is sourced from\n * the consuming site's breakpoints, so it is a string, not a fixed union — the\n * switcher resolves the available options via {@link resolveCanvasViewports}.\n */\nexport type Viewport = string\n\n/**\n * The builder store's action union. EC-287 retired the DB-resident project graph\n * and its persistence mirror, so these actions are pure in-memory editor state —\n * no action maps to a persisted `graph.*` command anymore. The block-field editor\n * lifts committed edits into the CMS document via `onChange`\n * (`composition-field-editor`); nothing persists a graph.\n */\nexport type BuilderAction =\n | { type: 'select'; id: string | null }\n | { type: 'hover'; id: string | null }\n | { type: 'viewport'; viewport: Viewport }\n | { type: 'undo' }\n | { type: 'redo' }\n | { type: 'insert'; target: InsertTarget; node: ComponentNode; select?: boolean }\n | { type: 'remove'; id: string }\n | { type: 'duplicate'; id: string }\n | { type: 'reorder'; id: string; delta: -1 | 1 }\n | { type: 'move'; id: string; target: InsertTarget }\n /**\n * EC-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: 'full',\n // Seed past the ids the composition already carries (EC-203): a restored\n // tree holds minted `node-<n>` ids, so starting at 0 would re-mint `node-1`\n // on the first insert and produce a duplicate-node-id.\n nodeSeq: maxNodeIdSeq(present),\n lastCoalesceKey: null,\n }\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"]}
@@ -0,0 +1,53 @@
1
+ import type { BreakpointConfig } from '@elytracms/core/studio-core';
2
+ /**
3
+ * EC-250 — canvas breakpoint preview, sourced from the consuming site's real
4
+ * widths instead of a baked-in desktop/tablet/phone triple.
5
+ *
6
+ * A `Viewport` is a preview-width control only (v1, EC-211): switching one
7
+ * resizes the preview frame so the author can verify components' OWN responsive
8
+ * behavior. It is NOT per-breakpoint authoring and writes no graph command.
9
+ */
10
+ /** One option the breakpoint switcher offers: an id, a label, and a CSS width. */
11
+ export interface ViewportOption {
12
+ /** The `builder.viewport` key this option sets ('full' or a breakpoint name). */
13
+ key: string;
14
+ label: string;
15
+ /** CSS width the canvas frame takes: '100%' for full, else `${width}px`. */
16
+ width: string;
17
+ /** Numeric min-width in px (absent for the full-width option). */
18
+ px?: number;
19
+ }
20
+ /**
21
+ * Tailwind's default `screens` scale. The documented fallback when a project
22
+ * declares no `breakpoints`: an unconfigured Tailwind site uses exactly these,
23
+ * so it is the site's real screens — not the arbitrary legacy triple.
24
+ */
25
+ export declare const TAILWIND_DEFAULT_SCREENS: readonly BreakpointConfig[];
26
+ export interface ResolvedViewports {
27
+ /** Full first, then each breakpoint ascending by width. Never empty. */
28
+ viewports: ViewportOption[];
29
+ /**
30
+ * A visible validation issue (no silent fallback) when a project DECLARED
31
+ * breakpoints but none are usable — the switcher shows Full only, with this
32
+ * surfaced beside it. Absent in the common (config or Tailwind-default) case.
33
+ */
34
+ issue?: string;
35
+ }
36
+ /**
37
+ * Resolve the breakpoint switcher's options from a project's configured
38
+ * breakpoints (EC-250):
39
+ * - `undefined` (none declared) → Tailwind's default screens, no issue,
40
+ * - declared + at least one usable → those (sorted ascending),
41
+ * - declared but none usable (empty / non-positive widths) → Full only + a
42
+ * visible issue, never the legacy desktop/tablet/phone triple.
43
+ * The full-width option is always present and first. Deterministic.
44
+ */
45
+ export declare function resolveCanvasViewports(configBreakpoints: readonly BreakpointConfig[] | undefined): ResolvedViewports;
46
+ /** The CSS width for the active viewport key, falling back to full. */
47
+ export declare function viewportWidth(viewports: ViewportOption[], activeKey: string): string;
48
+ /**
49
+ * The resolved breakpoint switcher for the active config project. The config is
50
+ * static per studio boot, so this memoizes on nothing — both the toolbar switcher
51
+ * and the canvas frame read the SAME resolved set (no drift).
52
+ */
53
+ export declare function useCanvasViewports(): ResolvedViewports;
@@ -0,0 +1,55 @@
1
+ import { useMemo } from 'react';
2
+ import { resolveDeploymentBreakpoints } from '@elytracms/studio/lib/workspace';
3
+ /**
4
+ * Tailwind's default `screens` scale. The documented fallback when a project
5
+ * declares no `breakpoints`: an unconfigured Tailwind site uses exactly these,
6
+ * so it is the site's real screens — not the arbitrary legacy triple.
7
+ */
8
+ export const TAILWIND_DEFAULT_SCREENS = [
9
+ { name: 'sm', width: 640 },
10
+ { name: 'md', width: 768 },
11
+ { name: 'lg', width: 1024 },
12
+ { name: 'xl', width: 1280 },
13
+ { name: '2xl', width: 1536 },
14
+ ];
15
+ /** The always-present unconstrained canvas width (the natural default). */
16
+ const FULL = { key: 'full', label: 'Full', width: '100%' };
17
+ /**
18
+ * Resolve the breakpoint switcher's options from a project's configured
19
+ * breakpoints (EC-250):
20
+ * - `undefined` (none declared) → Tailwind's default screens, no issue,
21
+ * - declared + at least one usable → those (sorted ascending),
22
+ * - declared but none usable (empty / non-positive widths) → Full only + a
23
+ * visible issue, never the legacy desktop/tablet/phone triple.
24
+ * The full-width option is always present and first. Deterministic.
25
+ */
26
+ export function resolveCanvasViewports(configBreakpoints) {
27
+ const source = configBreakpoints ?? TAILWIND_DEFAULT_SCREENS;
28
+ const usable = source.filter((b) => Number.isFinite(b.width) && b.width > 0);
29
+ if (configBreakpoints !== undefined && usable.length === 0) {
30
+ return {
31
+ viewports: [FULL],
32
+ issue: 'No usable canvas breakpoints — `breakpoints` is configured but empty or has no positive widths. Each needs a positive `width` in px.',
33
+ };
34
+ }
35
+ const sorted = [...usable].sort((a, b) => a.width - b.width);
36
+ return {
37
+ viewports: [
38
+ FULL,
39
+ ...sorted.map((b) => ({ key: b.name, label: b.label ?? b.name, width: `${b.width}px`, px: b.width })),
40
+ ],
41
+ };
42
+ }
43
+ /** The CSS width for the active viewport key, falling back to full. */
44
+ export function viewportWidth(viewports, activeKey) {
45
+ return (viewports.find((v) => v.key === activeKey) ?? viewports[0]).width;
46
+ }
47
+ /**
48
+ * The resolved breakpoint switcher for the active config project. The config is
49
+ * static per studio boot, so this memoizes on nothing — both the toolbar switcher
50
+ * and the canvas frame read the SAME resolved set (no drift).
51
+ */
52
+ export function useCanvasViewports() {
53
+ return useMemo(() => resolveCanvasViewports(resolveDeploymentBreakpoints()), []);
54
+ }
55
+ //# sourceMappingURL=canvas-breakpoints.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvas-breakpoints.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-breakpoints.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAE/B,OAAO,EAAE,4BAA4B,EAAE,MAAM,iCAAiC,CAAA;AAsB9E;;;;GAIG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAgC;IACnE,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE;IAC1B,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE;IAC1B,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;IAC3B,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;IAC3B,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE;CAC7B,CAAA;AAED,2EAA2E;AAC3E,MAAM,IAAI,GAAmB,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,CAAA;AAa1E;;;;;;;;GAQG;AACH,MAAM,UAAU,sBAAsB,CACpC,iBAA0D;IAE1D,MAAM,MAAM,GAAG,iBAAiB,IAAI,wBAAwB,CAAA;IAC5D,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAA;IAC5E,IAAI,iBAAiB,KAAK,SAAS,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3D,OAAO;YACL,SAAS,EAAE,CAAC,IAAI,CAAC;YACjB,KAAK,EACH,sIAAsI;SACzI,CAAA;IACH,CAAC;IACD,MAAM,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAA;IAC5D,OAAO;QACL,SAAS,EAAE;YACT,IAAI;YACJ,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC,KAAK,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;SACtG;KACF,CAAA;AACH,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,aAAa,CAAC,SAA2B,EAAE,SAAiB;IAC1E,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,SAAS,CAAC,IAAI,SAAS,CAAC,CAAC,CAAE,CAAC,CAAC,KAAK,CAAA;AAC5E,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB;IAChC,OAAO,OAAO,CAAC,GAAG,EAAE,CAAC,sBAAsB,CAAC,4BAA4B,EAAE,CAAC,EAAE,EAAE,CAAC,CAAA;AAClF,CAAC","sourcesContent":["import { useMemo } from 'react'\nimport type { BreakpointConfig } from '@elytracms/core/studio-core'\nimport { resolveDeploymentBreakpoints } from '@elytracms/studio/lib/workspace'\n\n/**\n * EC-250 — canvas breakpoint preview, sourced from the consuming site's real\n * widths instead of a baked-in desktop/tablet/phone triple.\n *\n * A `Viewport` is a preview-width control only (v1, EC-211): switching one\n * resizes the preview frame so the author can verify components' OWN responsive\n * behavior. It is NOT per-breakpoint authoring and writes no graph command.\n */\n\n/** One option the breakpoint switcher offers: an id, a label, and a CSS width. */\nexport interface ViewportOption {\n /** The `builder.viewport` key this option sets ('full' or a breakpoint name). */\n key: string\n label: string\n /** CSS width the canvas frame takes: '100%' for full, else `${width}px`. */\n width: string\n /** Numeric min-width in px (absent for the full-width option). */\n px?: number\n}\n\n/**\n * Tailwind's default `screens` scale. The documented fallback when a project\n * declares no `breakpoints`: an unconfigured Tailwind site uses exactly these,\n * so it is the site's real screens — not the arbitrary legacy triple.\n */\nexport const TAILWIND_DEFAULT_SCREENS: readonly BreakpointConfig[] = [\n { name: 'sm', width: 640 },\n { name: 'md', width: 768 },\n { name: 'lg', width: 1024 },\n { name: 'xl', width: 1280 },\n { name: '2xl', width: 1536 },\n]\n\n/** The always-present unconstrained canvas width (the natural default). */\nconst FULL: ViewportOption = { key: 'full', label: 'Full', width: '100%' }\n\nexport interface ResolvedViewports {\n /** Full first, then each breakpoint ascending by width. Never empty. */\n viewports: ViewportOption[]\n /**\n * A visible validation issue (no silent fallback) when a project DECLARED\n * breakpoints but none are usable — the switcher shows Full only, with this\n * surfaced beside it. Absent in the common (config or Tailwind-default) case.\n */\n issue?: string\n}\n\n/**\n * Resolve the breakpoint switcher's options from a project's configured\n * breakpoints (EC-250):\n * - `undefined` (none declared) → Tailwind's default screens, no issue,\n * - declared + at least one usable → those (sorted ascending),\n * - declared but none usable (empty / non-positive widths) → Full only + a\n * visible issue, never the legacy desktop/tablet/phone triple.\n * The full-width option is always present and first. Deterministic.\n */\nexport function resolveCanvasViewports(\n configBreakpoints: readonly BreakpointConfig[] | undefined,\n): ResolvedViewports {\n const source = configBreakpoints ?? TAILWIND_DEFAULT_SCREENS\n const usable = source.filter((b) => Number.isFinite(b.width) && b.width > 0)\n if (configBreakpoints !== undefined && usable.length === 0) {\n return {\n viewports: [FULL],\n issue:\n 'No usable canvas breakpoints — `breakpoints` is configured but empty or has no positive widths. Each needs a positive `width` in px.',\n }\n }\n const sorted = [...usable].sort((a, b) => a.width - b.width)\n return {\n viewports: [\n FULL,\n ...sorted.map((b) => ({ key: b.name, label: b.label ?? b.name, width: `${b.width}px`, px: b.width })),\n ],\n }\n}\n\n/** The CSS width for the active viewport key, falling back to full. */\nexport function viewportWidth(viewports: ViewportOption[], activeKey: string): string {\n return (viewports.find((v) => v.key === activeKey) ?? viewports[0]!).width\n}\n\n/**\n * The resolved breakpoint switcher for the active config project. The config is\n * static per studio boot, so this memoizes on nothing — both the toolbar switcher\n * and the canvas frame read the SAME resolved set (no drift).\n */\nexport function useCanvasViewports(): ResolvedViewports {\n return useMemo(() => resolveCanvasViewports(resolveDeploymentBreakpoints()), [])\n}\n"]}