@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.
- package/convex/cms.ts +8 -31
- package/convex/content.ts +3 -44
- package/convex/delivery.ts +8 -42
- package/convex/functions.ts +17 -2
- package/convex/members.ts +2 -2
- package/convex/referenceMaintenance.ts +4 -73
- package/convex/references.ts +4 -2
- package/convex/schema.ts +17 -37
- package/convex/sync.ts +10 -42
- package/convex/triggers.ts +57 -0
- package/convex/webhooks.ts +3 -2
- package/dist/components/app-views/default-modules.d.ts +41 -0
- package/dist/components/app-views/default-modules.js +201 -0
- package/dist/components/app-views/default-modules.js.map +1 -0
- package/dist/components/app-views/module.d.ts +93 -0
- package/dist/components/app-views/module.js +75 -0
- package/dist/components/app-views/module.js.map +1 -0
- package/dist/components/app-views/sidebar.d.ts +95 -0
- package/dist/components/app-views/sidebar.js +153 -0
- package/dist/components/app-views/sidebar.js.map +1 -0
- package/dist/components/builder/builder-store.d.ts +92 -40
- package/dist/components/builder/builder-store.js +30 -126
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/canvas-breakpoints.d.ts +53 -0
- package/dist/components/builder/canvas-breakpoints.js +55 -0
- package/dist/components/builder/canvas-breakpoints.js.map +1 -0
- package/dist/components/builder/canvas.js +35 -61
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/clipboard-actions.js +3 -3
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor.js +31 -40
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/insert-context.js +1 -1
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/page-tree.js +3 -4
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/property-inspector.js +4 -64
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/cms/bulk-actions.d.ts +23 -5
- package/dist/components/cms/bulk-actions.js +7 -3
- package/dist/components/cms/bulk-actions.js.map +1 -1
- package/dist/components/cms/bulk-bar.d.ts +22 -6
- package/dist/components/cms/bulk-bar.js +46 -9
- package/dist/components/cms/bulk-bar.js.map +1 -1
- package/dist/components/cms/cell.d.ts +0 -4
- package/dist/components/cms/cell.js +1 -5
- package/dist/components/cms/cell.js.map +1 -1
- package/dist/components/cms/cms-index.d.ts +4 -2
- package/dist/components/cms/cms-index.js +6 -4
- package/dist/components/cms/cms-index.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +7 -6
- package/dist/components/cms/cms-store.js +55 -18
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +8 -4
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/collections-sidebar.d.ts +10 -1
- package/dist/components/cms/collections-sidebar.js +13 -2
- package/dist/components/cms/collections-sidebar.js.map +1 -1
- package/dist/components/cms/detail-editor.js +2 -2
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/doc-state.d.ts +0 -5
- package/dist/components/cms/doc-state.js +1 -7
- package/dist/components/cms/doc-state.js.map +1 -1
- package/dist/components/cms/editable-cell.js +1 -1
- package/dist/components/cms/editable-cell.js.map +1 -1
- package/dist/components/cms/field-control.d.ts +11 -2
- package/dist/components/cms/field-control.js +22 -1
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/field-renderer.d.ts +71 -0
- package/dist/components/cms/field-renderer.js +67 -0
- package/dist/components/cms/field-renderer.js.map +1 -0
- package/dist/components/cms/filter-builder.js +6 -28
- package/dist/components/cms/filter-builder.js.map +1 -1
- package/dist/components/cms/scalar-value-editor.d.ts +35 -0
- package/dist/components/cms/scalar-value-editor.js +43 -0
- package/dist/components/cms/scalar-value-editor.js.map +1 -0
- package/dist/components/collab/collab-context.d.ts +6 -14
- package/dist/components/collab/collab-context.js +3 -12
- package/dist/components/collab/collab-context.js.map +1 -1
- package/dist/components/collab/collab-seed.d.ts +7 -19
- package/dist/components/collab/collab-seed.js +9 -31
- package/dist/components/collab/collab-seed.js.map +1 -1
- package/dist/components/components/registry-browser.js +6 -12
- package/dist/components/components/registry-browser.js.map +1 -1
- package/dist/components/guards.d.ts +0 -9
- package/dist/components/guards.js +0 -12
- package/dist/components/guards.js.map +1 -1
- package/dist/components/project-session.js +8 -3
- package/dist/components/project-session.js.map +1 -1
- package/dist/components/section-nav.d.ts +10 -10
- package/dist/components/section-nav.js +48 -25
- package/dist/components/section-nav.js.map +1 -1
- package/dist/components/section-state.d.ts +0 -2
- package/dist/components/section-state.js +0 -9
- package/dist/components/section-state.js.map +1 -1
- package/dist/components/section-surface.d.ts +3 -7
- package/dist/components/section-surface.js +6 -14
- package/dist/components/section-surface.js.map +1 -1
- package/dist/components/sections.d.ts +0 -1
- package/dist/components/sections.js +0 -10
- package/dist/components/sections.js.map +1 -1
- package/dist/components/studio-project-screen.d.ts +3 -4
- package/dist/components/studio-project-screen.js +41 -160
- package/dist/components/studio-project-screen.js.map +1 -1
- package/dist/components/studio-shell.d.ts +0 -4
- package/dist/components/studio-shell.js +14 -14
- package/dist/components/studio-shell.js.map +1 -1
- package/dist/lib/data/collection-content.d.ts +19 -1
- package/dist/lib/data/collection-content.js +16 -0
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/operations/graph-commands.d.ts +2 -6
- package/dist/lib/operations/graph-commands.js +1 -2
- package/dist/lib/operations/graph-commands.js.map +1 -1
- package/dist/lib/operations/index.d.ts +0 -2
- package/dist/lib/operations/index.js +0 -2
- package/dist/lib/operations/index.js.map +1 -1
- package/dist/lib/operations/session-hydration.d.ts +14 -24
- package/dist/lib/operations/session-hydration.js +15 -34
- package/dist/lib/operations/session-hydration.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.d.ts +1 -3
- package/dist/lib/persistence/convex-adapter.js +1 -154
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persona/labels.js +0 -2
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/references/reference-model.d.ts +0 -16
- package/dist/lib/references/reference-model.js +2 -54
- package/dist/lib/references/reference-model.js.map +1 -1
- package/dist/lib/references/references-store.js +3 -4
- package/dist/lib/references/references-store.js.map +1 -1
- package/dist/lib/registry/index.d.ts +0 -16
- package/dist/lib/registry/index.js +0 -15
- package/dist/lib/registry/index.js.map +1 -1
- package/dist/lib/webhooks/tags.js +2 -16
- package/dist/lib/webhooks/tags.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +27 -1
- package/dist/lib/workspace/index.js +34 -0
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/router/match.d.ts +22 -34
- package/dist/router/match.js +22 -40
- package/dist/router/match.js.map +1 -1
- package/package.json +3 -2
- package/convex/graphs.ts +0 -55
- 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,
|
|
3
|
+
import { cloneWithNewIds, duplicateNode, getNode, getParentInfo, insertNode, maxNodeIdSeq, moveNode, normalizeContainers, removeNode, removeNodeProp, reorderNode, setNodeProp, } from '@elytracms/core/project-graph';
|
|
4
4
|
import { applyWrap } from '@elytracms/studio/lib/dnd/drop-rules';
|
|
5
|
-
import { createGraphPersister, getStudioOperations } from '@elytracms/studio/lib/operations';
|
|
6
|
-
import { normalizeWorkingGraph, setWorkingPageSeo, toWorkingGraph } from '@elytracms/studio/lib/working-graph';
|
|
7
5
|
const HISTORY_LIMIT = 100;
|
|
8
|
-
function activeRoot(state) {
|
|
9
|
-
const page = state.present.pages.find((p) => p.id === state.pageId);
|
|
10
|
-
return page.root;
|
|
11
|
-
}
|
|
12
|
-
function withRoot(graph, pageId, root) {
|
|
13
|
-
return { ...graph, pages: graph.pages.map((p) => (p.id === pageId ? { ...p, root } : p)) };
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* The highest minted-id sequence already present in the working graph (EC-203).
|
|
17
|
-
* `nodeSeq` is a single session-wide counter feeding inserts on every page, so it
|
|
18
|
-
* must clear the max across ALL pages' roots — never just the active one — or
|
|
19
|
-
* switching pages and inserting would re-mint a collision on another page.
|
|
20
|
-
*/
|
|
21
|
-
function maxGraphNodeSeq(graph) {
|
|
22
|
-
return graph.pages.reduce((max, page) => Math.max(max, maxNodeIdSeq(page.root)), 0);
|
|
23
|
-
}
|
|
24
6
|
function counter(start) {
|
|
25
7
|
const c = {
|
|
26
8
|
count: start,
|
|
@@ -44,7 +26,7 @@ function counter(start) {
|
|
|
44
26
|
*/
|
|
45
27
|
function commit(state, root, opts = {}) {
|
|
46
28
|
const normalized = normalizeContainers(root);
|
|
47
|
-
const present =
|
|
29
|
+
const present = normalized;
|
|
48
30
|
const coalesce = opts.coalesceKey != null && opts.coalesceKey === state.lastCoalesceKey;
|
|
49
31
|
// Normalization can dissolve the node a caller wanted selected (e.g. the
|
|
50
32
|
// parent container of a removed child) — selection never points at a ghost.
|
|
@@ -66,16 +48,10 @@ function commit(state, root, opts = {}) {
|
|
|
66
48
|
* properties, not render behavior.
|
|
67
49
|
*/
|
|
68
50
|
export function builderReducer(state, action) {
|
|
69
|
-
const root =
|
|
51
|
+
const root = state.present;
|
|
70
52
|
switch (action.type) {
|
|
71
|
-
case 'select':
|
|
72
|
-
|
|
73
|
-
// that exist in the working graph; selection stays a pure-UI action.
|
|
74
|
-
const pageId = action.pageId !== undefined && state.present.pages.some((p) => p.id === action.pageId)
|
|
75
|
-
? action.pageId
|
|
76
|
-
: state.pageId;
|
|
77
|
-
return { ...state, selectedId: action.id, pageId };
|
|
78
|
-
}
|
|
53
|
+
case 'select':
|
|
54
|
+
return { ...state, selectedId: action.id };
|
|
79
55
|
case 'hover':
|
|
80
56
|
return state.hoveredId === action.id ? state : { ...state, hoveredId: action.id };
|
|
81
57
|
case 'viewport':
|
|
@@ -90,9 +66,7 @@ export function builderReducer(state, action) {
|
|
|
90
66
|
present: prev,
|
|
91
67
|
future: [state.present, ...state.future],
|
|
92
68
|
lastCoalesceKey: null,
|
|
93
|
-
selectedId: getNode(prev
|
|
94
|
-
? state.selectedId
|
|
95
|
-
: null,
|
|
69
|
+
selectedId: getNode(prev, state.selectedId ?? '') ? state.selectedId : null,
|
|
96
70
|
};
|
|
97
71
|
}
|
|
98
72
|
case 'redo': {
|
|
@@ -105,9 +79,7 @@ export function builderReducer(state, action) {
|
|
|
105
79
|
present: next,
|
|
106
80
|
future: state.future.slice(1),
|
|
107
81
|
lastCoalesceKey: null,
|
|
108
|
-
selectedId: getNode(next
|
|
109
|
-
? state.selectedId
|
|
110
|
-
: null,
|
|
82
|
+
selectedId: getNode(next, state.selectedId ?? '') ? state.selectedId : null,
|
|
111
83
|
};
|
|
112
84
|
}
|
|
113
85
|
case 'insert': {
|
|
@@ -161,116 +133,51 @@ export function builderReducer(state, action) {
|
|
|
161
133
|
});
|
|
162
134
|
case 'removeProp':
|
|
163
135
|
return commit(state, removeNodeProp(root, action.id, action.name));
|
|
164
|
-
case 'setCondition':
|
|
165
|
-
return commit(state, setNodeCondition(root, action.id, action.condition));
|
|
166
|
-
case 'setPageSeo': {
|
|
167
|
-
// Legacy page SEO (EC-171), in-memory only: update the working page's
|
|
168
|
-
// `seo` block. Undoable; selection unchanged; never persisted as a node
|
|
169
|
-
// op (page SEO lives on the page CMS document now).
|
|
170
|
-
const next = setWorkingPageSeo(state.present, action.pageId, action.seo);
|
|
171
|
-
if (next === state.present)
|
|
172
|
-
return state;
|
|
173
|
-
return {
|
|
174
|
-
...state,
|
|
175
|
-
past: [...state.past, state.present].slice(-HISTORY_LIMIT),
|
|
176
|
-
present: next,
|
|
177
|
-
future: [],
|
|
178
|
-
lastCoalesceKey: null,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
case 'loadGraph': {
|
|
182
|
-
// Replace the whole working graph (e.g. restoring a saved revision).
|
|
183
|
-
// `action.graph` is a CANONICAL layouts-only ProjectGraph (a restore from
|
|
184
|
-
// the append-only revision log); lift it into a WorkingGraph so the
|
|
185
|
-
// root-bearing editor has an editable page. Undoable, selection
|
|
186
|
-
// re-validated, normalized like the persisted graph.save it mirrors.
|
|
187
|
-
const graph = normalizeWorkingGraph(toWorkingGraph(action.graph));
|
|
188
|
-
const pageId = graph.pages.find((p) => p.id === state.pageId)
|
|
189
|
-
? state.pageId
|
|
190
|
-
: graph.pages[0].id;
|
|
191
|
-
const nextRoot = graph.pages.find((p) => p.id === pageId).root;
|
|
192
|
-
return {
|
|
193
|
-
...state,
|
|
194
|
-
past: [...state.past, state.present].slice(-HISTORY_LIMIT),
|
|
195
|
-
present: graph,
|
|
196
|
-
future: [],
|
|
197
|
-
pageId,
|
|
198
|
-
// Advance past the restored graph's ids too (EC-203), never backwards:
|
|
199
|
-
// a restore swaps in a fresh node namespace under the same counter.
|
|
200
|
-
nodeSeq: Math.max(state.nodeSeq, maxGraphNodeSeq(graph)),
|
|
201
|
-
lastCoalesceKey: null,
|
|
202
|
-
selectedId: getNode(nextRoot, state.selectedId ?? '') ? state.selectedId : nextRoot.id,
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
136
|
default:
|
|
206
137
|
return state;
|
|
207
138
|
}
|
|
208
139
|
}
|
|
209
140
|
const BuilderContext = createContext(null);
|
|
210
|
-
export function initialBuilderState(
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
// the persisted baseline agree from the first edit.
|
|
215
|
-
const working = 'pages' in graph ? graph : toWorkingGraph(graph);
|
|
216
|
-
const present = normalizeWorkingGraph(working);
|
|
141
|
+
export function initialBuilderState(composition) {
|
|
142
|
+
// Start container-normal (idempotent on healthy trees) so the session and the
|
|
143
|
+
// lifted baseline agree from the first edit.
|
|
144
|
+
const present = normalizeContainers(composition);
|
|
217
145
|
return {
|
|
218
146
|
past: [],
|
|
219
147
|
present,
|
|
220
148
|
future: [],
|
|
221
|
-
|
|
222
|
-
selectedId: present.pages[0].root.id,
|
|
149
|
+
selectedId: present.id,
|
|
223
150
|
hoveredId: null,
|
|
224
|
-
viewport: '
|
|
225
|
-
// Seed past the ids the
|
|
226
|
-
//
|
|
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:
|
|
155
|
+
nodeSeq: maxNodeIdSeq(present),
|
|
229
156
|
lastCoalesceKey: null,
|
|
230
157
|
};
|
|
231
158
|
}
|
|
232
159
|
/**
|
|
233
|
-
* Undo/redo design (EC-141): the history stack
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
* (`graph.save`, message "Undo"/"Redo"). This is the honest mapping of a
|
|
240
|
-
* linear session history onto an append-only revision log: the session and
|
|
241
|
-
* the persisted latest revision always agree, and the persisted timeline
|
|
242
|
-
* records that an undo happened rather than pretending it didn't.
|
|
243
|
-
*
|
|
244
|
-
* Without a `projectId` (styleguide, isolated tests) the store is a pure
|
|
245
|
-
* in-memory editor and persists nothing.
|
|
160
|
+
* Undo/redo design (EC-141): the history stack is *UI state* — an in-memory
|
|
161
|
+
* stack of composition-root snapshots. EC-287 retired the DB-resident project
|
|
162
|
+
* graph and its append-only revision log, so the store no longer mirrors edits onto a
|
|
163
|
+
* persistence layer: it is a pure in-memory editor. The block-field editor lifts
|
|
164
|
+
* its committed composition into the open CMS document via `onChange`
|
|
165
|
+
* (`composition-field-editor`); the document's own Save persists it as content.
|
|
246
166
|
*/
|
|
247
|
-
export function BuilderProvider({
|
|
248
|
-
const [state, setState] = useState(() => initialBuilderState(
|
|
249
|
-
const [persistError, setPersistError] = useState(null);
|
|
167
|
+
export function BuilderProvider({ composition, children, }) {
|
|
168
|
+
const [state, setState] = useState(() => initialBuilderState(composition));
|
|
250
169
|
const stateRef = useRef(state);
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
projectId,
|
|
255
|
-
getOperations: async () => (await getStudioOperations()).operations,
|
|
256
|
-
onError: setPersistError,
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
// Plain-function dispatch (not `useReducer`) so each action sees the
|
|
260
|
-
// pre/post state synchronously — the persister derives the mirrored
|
|
261
|
-
// `graph.*` command from exactly that transition.
|
|
170
|
+
// Plain-function dispatch (not `useReducer`) so each action sees the pre/post
|
|
171
|
+
// state synchronously. EC-287: nothing persists a graph — committed edits are
|
|
172
|
+
// lifted into the CMS document by the composition-field editor's `onChange`.
|
|
262
173
|
const dispatch = useCallback((action) => {
|
|
263
|
-
const
|
|
264
|
-
const next = builderReducer(prev, action);
|
|
174
|
+
const next = builderReducer(stateRef.current, action);
|
|
265
175
|
stateRef.current = next;
|
|
266
176
|
setState(next);
|
|
267
|
-
persisterRef.current?.enqueue(action, { present: prev.present, pageId: prev.pageId, nodeSeq: prev.nodeSeq }, { present: next.present });
|
|
268
177
|
}, []);
|
|
269
|
-
const flushPersistence = useCallback(async () => persisterRef.current?.flush() ?? Promise.resolve(), []);
|
|
270
|
-
const clearPersistError = useCallback(() => setPersistError(null), []);
|
|
271
178
|
const store = useMemo(() => {
|
|
272
179
|
const selectedNode = state.selectedId
|
|
273
|
-
? (getNode(
|
|
180
|
+
? (getNode(state.present, state.selectedId) ?? null)
|
|
274
181
|
: null;
|
|
275
182
|
return {
|
|
276
183
|
...state,
|
|
@@ -278,11 +185,8 @@ export function BuilderProvider({ graph = validSampleGraph, workingGraph, projec
|
|
|
278
185
|
canRedo: state.future.length > 0,
|
|
279
186
|
selectedNode,
|
|
280
187
|
dispatch,
|
|
281
|
-
persistError,
|
|
282
|
-
clearPersistError,
|
|
283
|
-
flushPersistence,
|
|
284
188
|
};
|
|
285
|
-
}, [state, dispatch
|
|
189
|
+
}, [state, dispatch]);
|
|
286
190
|
return _jsx(BuilderContext.Provider, { value: store, children: children });
|
|
287
191
|
}
|
|
288
192
|
export function useBuilder() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builder-store.js","sourceRoot":"","sources":["../../../src/components/builder/builder-store.tsx"],"names":[],"mappings":"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","sourcesContent":["import { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport {\n cloneWithNewIds,\n duplicateNode,\n getNode,\n getParentInfo,\n insertNode,\n maxNodeIdSeq,\n moveNode,\n normalizeContainers,\n removeNode,\n removeNodeProp,\n reorderNode,\n setNodeCondition,\n setNodeProp,\n validSampleGraph,\n} from '@elytracms/core/project-graph'\nimport type { ComponentNode, ProjectGraph } from '@elytracms/core/project-graph'\nimport { applyWrap } from '@elytracms/studio/lib/dnd/drop-rules'\nimport { createGraphPersister, getStudioOperations } from '@elytracms/studio/lib/operations'\nimport type { BuilderAction, GraphPersister, OperationError } from '@elytracms/studio/lib/operations'\nimport { normalizeWorkingGraph, setWorkingPageSeo, toWorkingGraph } from '@elytracms/studio/lib/working-graph'\nimport type { WorkingGraph } from '@elytracms/studio/lib/working-graph'\n\nconst HISTORY_LIMIT = 100\n\nexport type Viewport = 'desktop' | 'tablet' | 'mobile'\n\nexport interface BuilderState {\n past: WorkingGraph[]\n present: WorkingGraph\n future: WorkingGraph[]\n pageId: string\n selectedId: string | null\n hoveredId: string | null\n viewport: Viewport\n /** Monotonic counter feeding fresh node ids. */\n nodeSeq: number\n /** Coalescing key of the last edit (consecutive same-key edits don't stack history). */\n lastCoalesceKey: string | null\n}\n\ntype Action = BuilderAction\n\nfunction activeRoot(state: BuilderState): ComponentNode {\n const page = state.present.pages.find((p) => p.id === state.pageId)\n return page!.root\n}\n\nfunction withRoot(graph: WorkingGraph, pageId: string, root: ComponentNode): WorkingGraph {\n return { ...graph, pages: graph.pages.map((p) => (p.id === pageId ? { ...p, root } : p)) }\n}\n\n/**\n * The highest minted-id sequence already present in the working graph (EC-203).\n * `nodeSeq` is a single session-wide counter feeding inserts on every page, so it\n * must clear the max across ALL pages' roots — never just the active one — or\n * switching pages and inserting would re-mint a collision on another page.\n */\nfunction maxGraphNodeSeq(graph: WorkingGraph): number {\n return graph.pages.reduce((max, page) => Math.max(max, maxNodeIdSeq(page.root)), 0)\n}\n\ninterface Counter {\n genId: () => string\n count: number\n}\nfunction counter(start: number): Counter {\n const c: Counter = {\n count: start,\n genId: () => {\n c.count += 1\n return `node-${c.count}`\n },\n }\n return c\n}\n\n/**\n * Push a new graph onto history, coalescing consecutive edits with the same key.\n *\n * Every commit normalizes layout containers (EC-162 ledger fix): the\n * operations layer normalizes every persisted `graph.*` write (EC-161), so\n * the optimistic working graph must apply the same `normalizeContainers` pass\n * or the UI state diverges from the persisted revision after\n * container-emptying moves/removes. The mirrored command stays un-normalized\n * (`graphInvocationFor` sees the raw action); both sides normalize\n * identically, so they land on the same graph.\n */\nfunction commit(\n state: BuilderState,\n root: ComponentNode,\n opts: { coalesceKey?: string; nodeSeq?: number; selectedId?: string | null } = {},\n): BuilderState {\n const normalized = normalizeContainers(root)\n const present = withRoot(state.present, state.pageId, normalized)\n const coalesce = opts.coalesceKey != null && opts.coalesceKey === state.lastCoalesceKey\n // Normalization can dissolve the node a caller wanted selected (e.g. the\n // parent container of a removed child) — selection never points at a ghost.\n const desired = opts.selectedId !== undefined ? opts.selectedId : state.selectedId\n const selectedId = desired !== null && getNode(normalized, desired) ? desired : null\n return {\n ...state,\n past: coalesce ? state.past : [...state.past, state.present].slice(-HISTORY_LIMIT),\n present,\n future: [],\n nodeSeq: opts.nodeSeq ?? state.nodeSeq,\n lastCoalesceKey: opts.coalesceKey ?? null,\n selectedId,\n }\n}\n\n/**\n * The pure state transition. Exported (with `initialBuilderState`) for unit\n * tests — gesture determinism and normalization lockstep are reducer-level\n * properties, not render behavior.\n */\nexport function builderReducer(state: BuilderState, action: Action): BuilderState {\n const root = activeRoot(state)\n switch (action.type) {\n case 'select': {\n // Optional page switch (reference deep links, EC-172): only onto pages\n // that exist in the working graph; selection stays a pure-UI action.\n const pageId =\n action.pageId !== undefined && state.present.pages.some((p) => p.id === action.pageId)\n ? action.pageId\n : state.pageId\n return { ...state, selectedId: action.id, pageId }\n }\n case 'hover':\n return state.hoveredId === action.id ? state : { ...state, hoveredId: action.id }\n case 'viewport':\n return { ...state, viewport: action.viewport }\n\n case 'undo': {\n const prev = state.past.at(-1)\n if (!prev) return state\n return {\n ...state,\n past: state.past.slice(0, -1),\n present: prev,\n future: [state.present, ...state.future],\n lastCoalesceKey: null,\n selectedId: getNode(\n prev.pages.find((p) => p.id === state.pageId)!.root,\n state.selectedId ?? '',\n )\n ? state.selectedId\n : null,\n }\n }\n case 'redo': {\n const next = state.future[0]\n if (!next) return state\n return {\n ...state,\n past: [...state.past, state.present],\n present: next,\n future: state.future.slice(1),\n lastCoalesceKey: null,\n selectedId: getNode(\n next.pages.find((p) => p.id === state.pageId)!.root,\n state.selectedId ?? '',\n )\n ? state.selectedId\n : null,\n }\n }\n\n case 'insert': {\n const c = counter(state.nodeSeq)\n const node = cloneWithNewIds(action.node, c.genId)\n const next = insertNode(root, action.target, node)\n return commit(state, next, {\n nodeSeq: c.count,\n selectedId: action.select === false ? state.selectedId : node.id,\n })\n }\n case 'remove': {\n const parent = getParentInfo(root, action.id)\n const next = removeNode(root, action.id)\n const selectedId =\n state.selectedId === action.id ? (parent?.parent.id ?? null) : state.selectedId\n return commit(state, next, { selectedId })\n }\n case 'duplicate': {\n const c = counter(state.nodeSeq)\n const { root: next, newId } = duplicateNode(root, action.id, c.genId)\n return commit(state, next, { nodeSeq: c.count, selectedId: newId ?? state.selectedId })\n }\n case 'reorder':\n return commit(state, reorderNode(root, action.id, action.delta))\n case 'move':\n return commit(state, moveNode(root, action.id, action.target))\n case 'wrap': {\n // Layout gesture (EC-162): the reference and the dragged subject become\n // the two columns of a new Grid container — ONE structural edit, one\n // history entry, one mirrored command, so a single undo dissolves the\n // whole gesture.\n const c = counter(state.nodeSeq)\n const partner =\n action.subject.kind === 'node'\n ? { kind: 'node' as const, nodeId: action.subject.nodeId }\n : { kind: 'subtree' as const, node: cloneWithNewIds(action.subject.node, c.genId) }\n const containerId = c.genId()\n const wrapped = applyWrap({\n root,\n referenceId: action.referenceId,\n side: action.side,\n partner,\n containerId,\n })\n if (!wrapped) return state\n return commit(state, wrapped.root, { nodeSeq: c.count, selectedId: containerId })\n }\n case 'setProp':\n return commit(state, setNodeProp(root, action.id, action.name, action.value), {\n coalesceKey: action.coalesceKey,\n })\n case 'removeProp':\n return commit(state, removeNodeProp(root, action.id, action.name))\n case 'setCondition':\n return commit(state, setNodeCondition(root, action.id, action.condition))\n case 'setPageSeo': {\n // Legacy page SEO (EC-171), in-memory only: update the working page's\n // `seo` block. Undoable; selection unchanged; never persisted as a node\n // op (page SEO lives on the page CMS document now).\n const next = setWorkingPageSeo(state.present, action.pageId, action.seo)\n if (next === state.present) return state\n return {\n ...state,\n past: [...state.past, state.present].slice(-HISTORY_LIMIT),\n present: next,\n future: [],\n lastCoalesceKey: null,\n }\n }\n case 'loadGraph': {\n // Replace the whole working graph (e.g. restoring a saved revision).\n // `action.graph` is a CANONICAL layouts-only ProjectGraph (a restore from\n // the append-only revision log); lift it into a WorkingGraph so the\n // root-bearing editor has an editable page. Undoable, selection\n // re-validated, normalized like the persisted graph.save it mirrors.\n const graph = normalizeWorkingGraph(toWorkingGraph(action.graph))\n const pageId = graph.pages.find((p) => p.id === state.pageId)\n ? state.pageId\n : graph.pages[0]!.id\n const nextRoot = graph.pages.find((p) => p.id === pageId)!.root\n return {\n ...state,\n past: [...state.past, state.present].slice(-HISTORY_LIMIT),\n present: graph,\n future: [],\n pageId,\n // Advance past the restored graph's ids too (EC-203), never backwards:\n // a restore swaps in a fresh node namespace under the same counter.\n nodeSeq: Math.max(state.nodeSeq, maxGraphNodeSeq(graph)),\n lastCoalesceKey: null,\n selectedId: getNode(nextRoot, state.selectedId ?? '') ? state.selectedId : nextRoot.id,\n }\n }\n default:\n return state\n }\n}\n\nexport interface BuilderStore extends BuilderState {\n canUndo: boolean\n canRedo: boolean\n selectedNode: ComponentNode | null\n dispatch: (action: Action) => void\n /**\n * Most recent failed/denied persisted mutation (structured, from the\n * operation result). Rendered by the shell — never a silent failure.\n */\n persistError: OperationError | null\n clearPersistError: () => void\n /** Await all queued persisted mutations (publish flows, tests). */\n flushPersistence: () => Promise<void>\n}\n\nconst BuilderContext = createContext<BuilderStore | null>(null)\n\nexport function initialBuilderState(graph: ProjectGraph | WorkingGraph): BuilderState {\n // Lift a canonical (layouts-only) ProjectGraph into a root-bearing\n // WorkingGraph; a WorkingGraph (synthetic composition surface) is used as-is.\n // Start container-normal (idempotent on healthy graphs) so the session and\n // the persisted baseline agree from the first edit.\n const working = 'pages' in graph ? graph : toWorkingGraph(graph)\n const present = normalizeWorkingGraph(working)\n return {\n past: [],\n present,\n future: [],\n pageId: present.pages[0]!.id,\n selectedId: present.pages[0]!.root.id,\n hoveredId: null,\n viewport: 'desktop',\n // Seed past the ids the hydrated graph already carries (EC-203): a restored\n // page holds minted `node-<n>` ids, so starting at 0 would re-mint `node-1`\n // on the first insert and produce a duplicate-node-id.\n nodeSeq: maxGraphNodeSeq(present),\n lastCoalesceKey: null,\n }\n}\n\n/**\n * Undo/redo design (EC-141): the history stack stays *UI state* — an\n * in-memory stack of working-graph snapshots, exactly as before. The graph\n * domain is append-only (load-latest → edit → save-revision), so a persisted\n * \"undo\" cannot rewind the timeline; instead every committed edit is mirrored\n * onto the operations layer as the matching `graph.*` command, and an\n * undo/redo persists the restored snapshot as a *new* revision\n * (`graph.save`, message \"Undo\"/\"Redo\"). This is the honest mapping of a\n * linear session history onto an append-only revision log: the session and\n * the persisted latest revision always agree, and the persisted timeline\n * records that an undo happened rather than pretending it didn't.\n *\n * Without a `projectId` (styleguide, isolated tests) the store is a pure\n * in-memory editor and persists nothing.\n */\nexport function BuilderProvider({\n graph = validSampleGraph,\n workingGraph,\n projectId,\n children,\n}: {\n /** Canonical (layouts-only) project graph; lifted into a working graph. */\n graph?: ProjectGraph\n /**\n * A pre-built working graph (the composition-field editor's synthetic page);\n * used as-is when present, overriding `graph`.\n */\n workingGraph?: WorkingGraph\n /** Enables persistence through the shared operations client. */\n projectId?: string\n children: ReactNode\n}) {\n const [state, setState] = useState<BuilderState>(() =>\n initialBuilderState(workingGraph ?? graph),\n )\n const [persistError, setPersistError] = useState<OperationError | null>(null)\n const stateRef = useRef(state)\n\n const persisterRef = useRef<GraphPersister | null>(null)\n if (persisterRef.current === null && projectId !== undefined) {\n persisterRef.current = createGraphPersister({\n projectId,\n getOperations: async () => (await getStudioOperations()).operations,\n onError: setPersistError,\n })\n }\n\n // Plain-function dispatch (not `useReducer`) so each action sees the\n // pre/post state synchronously — the persister derives the mirrored\n // `graph.*` command from exactly that transition.\n const dispatch = useCallback((action: Action) => {\n const prev = stateRef.current\n const next = builderReducer(prev, action)\n stateRef.current = next\n setState(next)\n persisterRef.current?.enqueue(\n action,\n { present: prev.present, pageId: prev.pageId, nodeSeq: prev.nodeSeq },\n { present: next.present },\n )\n }, [])\n\n const flushPersistence = useCallback(\n async () => persisterRef.current?.flush() ?? Promise.resolve(),\n [],\n )\n const clearPersistError = useCallback(() => setPersistError(null), [])\n\n const store = useMemo<BuilderStore>(() => {\n const selectedNode = state.selectedId\n ? (getNode(activeRoot(state), state.selectedId) ?? null)\n : null\n return {\n ...state,\n canUndo: state.past.length > 0,\n canRedo: state.future.length > 0,\n selectedNode,\n dispatch,\n persistError,\n clearPersistError,\n flushPersistence,\n }\n }, [state, dispatch, persistError, clearPersistError, flushPersistence])\n\n return <BuilderContext.Provider value={store}>{children}</BuilderContext.Provider>\n}\n\nexport function useBuilder(): BuilderStore {\n const store = useContext(BuilderContext)\n if (!store) throw new Error('useBuilder must be used within a <BuilderProvider>.')\n return store\n}\n"]}
|
|
1
|
+
{"version":3,"file":"builder-store.js","sourceRoot":"","sources":["../../../src/components/builder/builder-store.tsx"],"names":[],"mappings":"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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"]}
|