@elytracms/studio 0.0.37 → 0.0.39
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 -2
- package/dist/components/app-views/default-modules.d.ts +60 -5
- package/dist/components/app-views/default-modules.js +53 -15
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/app-views/module.d.ts +2 -0
- package/dist/components/app-views/module.js +8 -0
- package/dist/components/app-views/module.js.map +1 -1
- package/dist/components/assets/asset-detail.d.ts +24 -0
- package/dist/components/assets/asset-detail.js +163 -0
- package/dist/components/assets/asset-detail.js.map +1 -0
- package/dist/components/assets/asset-grid.d.ts +28 -0
- package/dist/components/assets/asset-grid.js +68 -0
- package/dist/components/assets/asset-grid.js.map +1 -0
- package/dist/components/assets/asset-metadata.d.ts +19 -0
- package/dist/components/assets/asset-metadata.js +44 -0
- package/dist/components/assets/asset-metadata.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +54 -0
- package/dist/components/assets/assets-collection-view-source.js +186 -0
- package/dist/components/assets/assets-collection-view-source.js.map +1 -0
- package/dist/components/assets/assets-module.d.ts +18 -0
- package/dist/components/assets/assets-module.js +94 -0
- package/dist/components/assets/assets-module.js.map +1 -0
- package/dist/components/assets/assets-store.js +2 -7
- package/dist/components/assets/assets-store.js.map +1 -1
- package/dist/components/assets/assets-view-preference.d.ts +15 -0
- package/dist/components/assets/assets-view-preference.js +22 -0
- package/dist/components/assets/assets-view-preference.js.map +1 -0
- package/dist/components/builder/builder-store.d.ts +12 -0
- package/dist/components/builder/builder-store.js +18 -16
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/clipboard-actions.js +5 -14
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor-lazy.d.ts +19 -0
- package/dist/components/builder/composition-field-editor-lazy.js +29 -0
- package/dist/components/builder/composition-field-editor-lazy.js.map +1 -0
- package/dist/components/builder/composition-field-editor.js +1 -4
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/insert-context.d.ts +8 -0
- package/dist/components/builder/insert-context.js +1 -3
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/page-tree.js +1 -3
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/property-inspector.js +7 -134
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +15 -4
- package/dist/components/cms/cms-store.js +42 -24
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.d.ts +8 -1
- package/dist/components/cms/collection-table.js +62 -10
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/column-width.d.ts +25 -0
- package/dist/components/cms/column-width.js +47 -0
- package/dist/components/cms/column-width.js.map +1 -0
- package/dist/components/cms/detail-editor.js +101 -68
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/discard-draft.d.ts +32 -0
- package/dist/components/cms/discard-draft.js +13 -0
- package/dist/components/cms/discard-draft.js.map +1 -0
- package/dist/components/cms/document-staging.d.ts +7 -0
- package/dist/components/cms/document-staging.js +44 -0
- package/dist/components/cms/document-staging.js.map +1 -0
- package/dist/components/cms/field-connection.d.ts +22 -0
- package/dist/components/cms/field-connection.js +37 -0
- package/dist/components/cms/field-connection.js.map +1 -0
- package/dist/components/cms/field-control.js +158 -34
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/prop-fields.d.ts +43 -0
- package/dist/components/cms/prop-fields.js +130 -0
- package/dist/components/cms/prop-fields.js.map +1 -0
- package/dist/components/cms/rich-text/component-embed.d.ts +17 -3
- package/dist/components/cms/rich-text/component-embed.js +29 -7
- package/dist/components/cms/rich-text/component-embed.js.map +1 -1
- package/dist/components/cms/rich-text/editor.d.ts +3 -0
- package/dist/components/cms/rich-text/editor.js +21 -3
- package/dist/components/cms/rich-text/editor.js.map +1 -1
- package/dist/components/cms/rich-text/embed-picker.d.ts +17 -0
- package/dist/components/cms/rich-text/embed-picker.js +34 -0
- package/dist/components/cms/rich-text/embed-picker.js.map +1 -0
- package/dist/components/cms/rich-text/embed-props.d.ts +49 -0
- package/dist/components/cms/rich-text/embed-props.js +106 -0
- package/dist/components/cms/rich-text/embed-props.js.map +1 -0
- package/dist/components/cms/rich-text/extensions.d.ts +7 -1
- package/dist/components/cms/rich-text/extensions.js +6 -1
- package/dist/components/cms/rich-text/extensions.js.map +1 -1
- package/dist/components/cms/rich-text/index.d.ts +6 -1
- package/dist/components/cms/rich-text/index.js +6 -1
- package/dist/components/cms/rich-text/index.js.map +1 -1
- package/dist/components/cms/rich-text/insert.d.ts +21 -0
- package/dist/components/cms/rich-text/insert.js +55 -0
- package/dist/components/cms/rich-text/insert.js.map +1 -0
- package/dist/components/cms/rich-text/lazy.d.ts +19 -0
- package/dist/components/cms/rich-text/lazy.js +26 -0
- package/dist/components/cms/rich-text/lazy.js.map +1 -0
- package/dist/components/cms/rich-text/slash-command.d.ts +27 -0
- package/dist/components/cms/rich-text/slash-command.js +158 -0
- package/dist/components/cms/rich-text/slash-command.js.map +1 -0
- package/dist/components/cms/rich-text/toolbar.d.ts +5 -6
- package/dist/components/cms/rich-text/toolbar.js +3 -5
- package/dist/components/cms/rich-text/toolbar.js.map +1 -1
- package/dist/components/cms/row-actions.d.ts +28 -0
- package/dist/components/cms/row-actions.js +20 -0
- package/dist/components/cms/row-actions.js.map +1 -0
- package/dist/components/cms/use-document-versions.d.ts +8 -1
- package/dist/components/cms/use-document-versions.js +31 -4
- package/dist/components/cms/use-document-versions.js.map +1 -1
- package/dist/components/cms/version-compare-lazy.d.ts +1 -0
- package/dist/components/cms/version-compare-lazy.js +23 -0
- package/dist/components/cms/version-compare-lazy.js.map +1 -0
- package/dist/components/cms/version-compare-text.d.ts +46 -0
- package/dist/components/cms/version-compare-text.js +78 -0
- package/dist/components/cms/version-compare-text.js.map +1 -1
- package/dist/components/cms/version-compare.d.ts +9 -5
- package/dist/components/cms/version-compare.js +184 -59
- package/dist/components/cms/version-compare.js.map +1 -1
- package/dist/components/cms/versions-pane.js +10 -4
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/components/component-detail.d.ts +5 -3
- package/dist/components/components/component-detail.js +41 -8
- package/dist/components/components/component-detail.js.map +1 -1
- package/dist/components/components/component-preview.d.ts +61 -0
- package/dist/components/components/component-preview.js +103 -0
- package/dist/components/components/component-preview.js.map +1 -0
- package/dist/components/components/components-index.d.ts +5 -3
- package/dist/components/components/components-index.js +19 -24
- package/dist/components/components/components-index.js.map +1 -1
- package/dist/components/components/registry-browser.d.ts +7 -0
- package/dist/components/components/registry-browser.js +20 -36
- package/dist/components/components/registry-browser.js.map +1 -1
- package/dist/components/components/taxonomy.d.ts +51 -0
- package/dist/components/components/taxonomy.js +73 -0
- package/dist/components/components/taxonomy.js.map +1 -0
- package/dist/components/components/transfer-dialog.d.ts +20 -0
- package/dist/components/components/transfer-dialog.js +64 -0
- package/dist/components/components/transfer-dialog.js.map +1 -0
- package/dist/components/components/transfer.d.ts +43 -0
- package/dist/components/components/transfer.js +31 -0
- package/dist/components/components/transfer.js.map +1 -0
- package/dist/components/project-list.js +3 -4
- package/dist/components/project-list.js.map +1 -1
- package/dist/components/project-switcher.js +3 -2
- package/dist/components/project-switcher.js.map +1 -1
- package/dist/components/rail-store-status.d.ts +21 -0
- package/dist/components/rail-store-status.js +21 -0
- package/dist/components/rail-store-status.js.map +1 -0
- package/dist/components/redirects/redirects-module.js +4 -8
- package/dist/components/redirects/redirects-module.js.map +1 -1
- package/dist/components/redirects/redirects-store.js +2 -7
- package/dist/components/redirects/redirects-store.js.map +1 -1
- package/dist/components/sign-in-needed-badge.d.ts +6 -0
- package/dist/components/sign-in-needed-badge.js +13 -0
- package/dist/components/sign-in-needed-badge.js.map +1 -0
- package/dist/components/studio-project-screen.js +1 -4
- package/dist/components/studio-project-screen.js.map +1 -1
- package/dist/components/studio-shell.js +1 -4
- package/dist/components/studio-shell.js.map +1 -1
- package/dist/components/users/users-module.js +5 -7
- package/dist/components/users/users-module.js.map +1 -1
- package/dist/components/users/users-store.js +2 -7
- package/dist/components/users/users-store.js.map +1 -1
- package/dist/lib/auth/index.js +1 -1
- package/dist/lib/auth/index.js.map +1 -1
- package/dist/lib/clipboard/paste-target.js +1 -3
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +0 -2
- package/dist/lib/cms-views/view-model.js +0 -7
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/diff-view-preference.d.ts +7 -0
- package/dist/lib/diff-view-preference.js +21 -0
- package/dist/lib/diff-view-preference.js.map +1 -0
- package/dist/lib/i18n/messages.en.js +81 -30
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/local-preference.d.ts +11 -0
- package/dist/lib/local-preference.js +26 -0
- package/dist/lib/local-preference.js.map +1 -0
- package/dist/lib/locale/fallback.d.ts +45 -0
- package/dist/lib/locale/fallback.js +85 -0
- package/dist/lib/locale/fallback.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +6 -0
- package/dist/lib/persona/labels.js +8 -0
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/query/index.d.ts +1 -1
- package/dist/lib/query/index.js +1 -1
- package/dist/lib/query/index.js.map +1 -1
- package/dist/lib/query/operations-query.d.ts +14 -2
- package/dist/lib/query/operations-query.js +11 -6
- package/dist/lib/query/operations-query.js.map +1 -1
- package/dist/lib/rail.d.ts +0 -6
- package/dist/lib/rail.js +6 -25
- package/dist/lib/rail.js.map +1 -1
- package/dist/lib/theme.d.ts +0 -6
- package/dist/lib/theme.js +10 -13
- package/dist/lib/theme.js.map +1 -1
- package/dist/router/match.d.ts +1 -0
- package/dist/router/match.js +4 -0
- package/dist/router/match.js.map +1 -1
- package/dist/ui/button.d.ts +1 -1
- package/package.json +22 -24
- package/src/styles/studio.css +17 -0
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { createContext,
|
|
2
|
+
import { createContext, useContext, useMemo, useReducer } from 'react';
|
|
3
3
|
import { cloneWithNewIds, duplicateNode, getNode, getParentInfo, insertNode, maxNodeIdSeq, moveNode, normalizeContainers, removeNode, removeNodeProp, reorderNode, replaceNode, setNodeProp, } from '@elytracms/core/project-graph';
|
|
4
4
|
import { applyWrap } from '@elytracms/studio/lib/dnd/drop-rules';
|
|
5
5
|
const HISTORY_LIMIT = 100;
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* The single fresh-node-id counter (`node-<n>`), shared by the reducer's structural
|
|
8
|
+
* actions AND the clipboard paste path (`clipboard-actions.tsx`) so the paste-issue
|
|
9
|
+
* deep links can mirror the exact ids the reducer will mint — one implementation,
|
|
10
|
+
* no drift. `count` mutates as `genId` is called, so the caller reads it afterward
|
|
11
|
+
* to advance `nodeSeq`.
|
|
12
|
+
*/
|
|
13
|
+
export function makeNodeIdCounter(start) {
|
|
7
14
|
const c = {
|
|
8
15
|
count: start,
|
|
9
16
|
genId: () => {
|
|
@@ -83,7 +90,7 @@ export function builderReducer(state, action) {
|
|
|
83
90
|
};
|
|
84
91
|
}
|
|
85
92
|
case 'insert': {
|
|
86
|
-
const c =
|
|
93
|
+
const c = makeNodeIdCounter(state.nodeSeq);
|
|
87
94
|
const node = cloneWithNewIds(action.node, c.genId);
|
|
88
95
|
const next = insertNode(root, action.target, node);
|
|
89
96
|
return commit(state, next, {
|
|
@@ -98,7 +105,7 @@ export function builderReducer(state, action) {
|
|
|
98
105
|
return commit(state, next, { selectedId });
|
|
99
106
|
}
|
|
100
107
|
case 'duplicate': {
|
|
101
|
-
const c =
|
|
108
|
+
const c = makeNodeIdCounter(state.nodeSeq);
|
|
102
109
|
const { root: next, newId } = duplicateNode(root, action.id, c.genId);
|
|
103
110
|
return commit(state, next, { nodeSeq: c.count, selectedId: newId ?? state.selectedId });
|
|
104
111
|
}
|
|
@@ -110,7 +117,7 @@ export function builderReducer(state, action) {
|
|
|
110
117
|
// Mint fresh ids for the incoming nodes (page-local namespace) so a detached
|
|
111
118
|
// copy never collides with the page, and a promoted block's source subtree
|
|
112
119
|
// (kept for the new reusable document) stays untouched.
|
|
113
|
-
const c =
|
|
120
|
+
const c = makeNodeIdCounter(state.nodeSeq);
|
|
114
121
|
const cloned = action.nodes.map((n) => cloneWithNewIds(n, c.genId));
|
|
115
122
|
const next = replaceNode(root, action.id, cloned);
|
|
116
123
|
if (next === root)
|
|
@@ -125,7 +132,7 @@ export function builderReducer(state, action) {
|
|
|
125
132
|
// the two columns of a new Grid container — ONE structural edit, one
|
|
126
133
|
// history entry, one mirrored command, so a single undo dissolves the
|
|
127
134
|
// whole gesture.
|
|
128
|
-
const c =
|
|
135
|
+
const c = makeNodeIdCounter(state.nodeSeq);
|
|
129
136
|
const partner = action.subject.kind === 'node'
|
|
130
137
|
? { kind: 'node', nodeId: action.subject.nodeId }
|
|
131
138
|
: { kind: 'subtree', node: cloneWithNewIds(action.subject.node, c.genId) };
|
|
@@ -179,16 +186,11 @@ export function initialBuilderState(composition) {
|
|
|
179
186
|
* (`composition-field-editor`); the document's own Save persists it as content.
|
|
180
187
|
*/
|
|
181
188
|
export function BuilderProvider({ composition, children, }) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
const dispatch = useCallback((action) => {
|
|
188
|
-
const next = builderReducer(stateRef.current, action);
|
|
189
|
-
stateRef.current = next;
|
|
190
|
-
setState(next);
|
|
191
|
-
}, []);
|
|
189
|
+
// EC-287: nothing persists a graph — committed edits are lifted into the CMS
|
|
190
|
+
// document by the composition-field editor's `onChange`. `useReducer` drives the
|
|
191
|
+
// pure `builderReducer` (consecutive dispatches in one gesture apply in sequence);
|
|
192
|
+
// the store below decorates the state with the derived undo/redo + selection flags.
|
|
193
|
+
const [state, dispatch] = useReducer(builderReducer, composition, initialBuilderState);
|
|
192
194
|
const store = useMemo(() => {
|
|
193
195
|
const selectedNode = state.selectedId
|
|
194
196
|
? (getNode(state.present, state.selectedId) ?? null)
|
|
@@ -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 replaceNode,\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-282 reuse gesture: replace a node with zero or more nodes at its\n * position (clone-with-fresh-ids, like `insert`). Promote → 1 embed node;\n * detach → the referenced element's inline blocks. One atomic structural\n * edit, one undo step.\n */\n | { type: 'replace'; id: string; nodes: ComponentNode[]; select?: boolean }\n /**\n * EC-162 layout gesture: wrap the reference node and the dragged subject in\n * a two-column Grid container as ONE atomic structural edit (one undo step).\n */\n | {\n type: 'wrap'\n referenceId: string\n side: 'left' | 'right'\n subject: { kind: 'node'; nodeId: string } | { kind: 'component'; node: ComponentNode }\n }\n | { type: 'setProp'; id: string; name: string; value: PropValue; coalesceKey?: string }\n | { type: 'removeProp'; id: string; name: string }\n\nexport interface BuilderState {\n /**\n * The edited composition: one root-bearing component tree (EC-287). The canvas\n * wraps a page document's `body` blocks into a synthetic root and edits it\n * directly — no page/layout wrapper. Undo/redo snapshot this root.\n */\n past: ComponentNode[]\n present: ComponentNode\n future: ComponentNode[]\n selectedId: string | null\n hoveredId: string | null\n viewport: Viewport\n /** Monotonic counter feeding fresh node ids. */\n nodeSeq: number\n /** Coalescing key of the last edit (consecutive same-key edits don't stack history). */\n lastCoalesceKey: string | null\n}\n\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 'replace': {\n // Mint fresh ids for the incoming nodes (page-local namespace) so a detached\n // copy never collides with the page, and a promoted block's source subtree\n // (kept for the new reusable document) stays untouched.\n const c = counter(state.nodeSeq)\n const cloned = action.nodes.map((n) => cloneWithNewIds(n, c.genId))\n const next = replaceNode(root, action.id, cloned)\n if (next === root) return state // root or absent id — no-op\n return commit(state, next, {\n nodeSeq: c.count,\n selectedId: action.select === false ? state.selectedId : (cloned[0]?.id ?? state.selectedId),\n })\n }\n case 'wrap': {\n // Layout gesture (EC-162): the reference and the dragged subject become\n // the two columns of a new Grid container — ONE structural edit, one\n // history entry, one mirrored command, so a single undo dissolves the\n // whole gesture.\n const c = 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"]}
|
|
1
|
+
{"version":3,"file":"builder-store.js","sourceRoot":"","sources":["../../../src/components/builder/builder-store.tsx"],"names":[],"mappings":"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","sourcesContent":["import { createContext, useContext, useMemo, useReducer } from 'react'\nimport type { ReactNode } from 'react'\nimport {\n cloneWithNewIds,\n duplicateNode,\n getNode,\n getParentInfo,\n insertNode,\n maxNodeIdSeq,\n moveNode,\n normalizeContainers,\n removeNode,\n removeNodeProp,\n reorderNode,\n replaceNode,\n setNodeProp,\n} from '@elytracms/core/project-graph'\nimport type { ComponentNode, InsertTarget, 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-282 reuse gesture: replace a node with zero or more nodes at its\n * position (clone-with-fresh-ids, like `insert`). Promote → 1 embed node;\n * detach → the referenced element's inline blocks. One atomic structural\n * edit, one undo step.\n */\n | { type: 'replace'; id: string; nodes: ComponentNode[]; select?: boolean }\n /**\n * EC-162 layout gesture: wrap the reference node and the dragged subject in\n * a two-column Grid container as ONE atomic structural edit (one undo step).\n */\n | {\n type: 'wrap'\n referenceId: string\n side: 'left' | 'right'\n subject: { kind: 'node'; nodeId: string } | { kind: 'component'; node: ComponentNode }\n }\n | { type: 'setProp'; id: string; name: string; value: PropValue; coalesceKey?: string }\n | { type: 'removeProp'; id: string; name: string }\n\nexport interface BuilderState {\n /**\n * The edited composition: one root-bearing component tree (EC-287). The canvas\n * wraps a page document's `body` blocks into a synthetic root and edits it\n * directly — no page/layout wrapper. Undo/redo snapshot this root.\n */\n past: ComponentNode[]\n present: ComponentNode\n future: ComponentNode[]\n selectedId: string | null\n hoveredId: string | null\n viewport: Viewport\n /** Monotonic counter feeding fresh node ids. */\n nodeSeq: number\n /** Coalescing key of the last edit (consecutive same-key edits don't stack history). */\n lastCoalesceKey: string | null\n}\n\ntype Action = BuilderAction\n\nexport interface Counter {\n genId: () => string\n count: number\n}\n/**\n * The single fresh-node-id counter (`node-<n>`), shared by the reducer's structural\n * actions AND the clipboard paste path (`clipboard-actions.tsx`) so the paste-issue\n * deep links can mirror the exact ids the reducer will mint — one implementation,\n * no drift. `count` mutates as `genId` is called, so the caller reads it afterward\n * to advance `nodeSeq`.\n */\nexport function makeNodeIdCounter(start: number): Counter {\n const c: Counter = {\n count: start,\n genId: () => {\n c.count += 1\n return `node-${c.count}`\n },\n }\n return c\n}\n\n/**\n * Push a new graph onto history, coalescing consecutive edits with the same key.\n *\n * Every commit normalizes layout containers (EC-162 ledger fix): the\n * operations layer normalizes every persisted `graph.*` write (EC-161), so\n * the optimistic working graph must apply the same `normalizeContainers` pass\n * or the UI state diverges from the persisted revision after\n * container-emptying moves/removes. The mirrored command stays un-normalized\n * (`graphInvocationFor` sees the raw action); both sides normalize\n * identically, so they land on the same graph.\n */\nfunction commit(\n state: BuilderState,\n root: ComponentNode,\n opts: { coalesceKey?: string; nodeSeq?: number; selectedId?: string | null } = {},\n): BuilderState {\n const normalized = normalizeContainers(root)\n const present = normalized\n const coalesce = opts.coalesceKey != null && opts.coalesceKey === state.lastCoalesceKey\n // Normalization can dissolve the node a caller wanted selected (e.g. the\n // parent container of a removed child) — selection never points at a ghost.\n const desired = opts.selectedId !== undefined ? opts.selectedId : state.selectedId\n const selectedId = desired !== null && getNode(normalized, desired) ? desired : null\n return {\n ...state,\n past: coalesce ? state.past : [...state.past, state.present].slice(-HISTORY_LIMIT),\n present,\n future: [],\n nodeSeq: opts.nodeSeq ?? state.nodeSeq,\n lastCoalesceKey: opts.coalesceKey ?? null,\n selectedId,\n }\n}\n\n/**\n * The pure state transition. Exported (with `initialBuilderState`) for unit\n * tests — gesture determinism and normalization lockstep are reducer-level\n * properties, not render behavior.\n */\nexport function builderReducer(state: BuilderState, action: Action): BuilderState {\n const root = state.present\n switch (action.type) {\n case 'select':\n return { ...state, selectedId: action.id }\n case 'hover':\n return state.hoveredId === action.id ? state : { ...state, hoveredId: action.id }\n case 'viewport':\n return { ...state, viewport: action.viewport }\n\n case 'undo': {\n const prev = state.past.at(-1)\n if (!prev) return state\n return {\n ...state,\n past: state.past.slice(0, -1),\n present: prev,\n future: [state.present, ...state.future],\n lastCoalesceKey: null,\n selectedId: getNode(prev, state.selectedId ?? '') ? state.selectedId : null,\n }\n }\n case 'redo': {\n const next = state.future[0]\n if (!next) return state\n return {\n ...state,\n past: [...state.past, state.present],\n present: next,\n future: state.future.slice(1),\n lastCoalesceKey: null,\n selectedId: getNode(next, state.selectedId ?? '') ? state.selectedId : null,\n }\n }\n\n case 'insert': {\n const c = makeNodeIdCounter(state.nodeSeq)\n const node = cloneWithNewIds(action.node, c.genId)\n const next = insertNode(root, action.target, node)\n return commit(state, next, {\n nodeSeq: c.count,\n selectedId: action.select === false ? state.selectedId : node.id,\n })\n }\n case 'remove': {\n const parent = getParentInfo(root, action.id)\n const next = removeNode(root, action.id)\n const selectedId =\n state.selectedId === action.id ? (parent?.parent.id ?? null) : state.selectedId\n return commit(state, next, { selectedId })\n }\n case 'duplicate': {\n const c = makeNodeIdCounter(state.nodeSeq)\n const { root: next, newId } = duplicateNode(root, action.id, c.genId)\n return commit(state, next, { nodeSeq: c.count, selectedId: newId ?? state.selectedId })\n }\n case 'reorder':\n return commit(state, reorderNode(root, action.id, action.delta))\n case 'move':\n return commit(state, moveNode(root, action.id, action.target))\n case 'replace': {\n // Mint fresh ids for the incoming nodes (page-local namespace) so a detached\n // copy never collides with the page, and a promoted block's source subtree\n // (kept for the new reusable document) stays untouched.\n const c = makeNodeIdCounter(state.nodeSeq)\n const cloned = action.nodes.map((n) => cloneWithNewIds(n, c.genId))\n const next = replaceNode(root, action.id, cloned)\n if (next === root) return state // root or absent id — no-op\n return commit(state, next, {\n nodeSeq: c.count,\n selectedId: action.select === false ? state.selectedId : (cloned[0]?.id ?? state.selectedId),\n })\n }\n case 'wrap': {\n // Layout gesture (EC-162): the reference and the dragged subject become\n // the two columns of a new Grid container — ONE structural edit, one\n // history entry, one mirrored command, so a single undo dissolves the\n // whole gesture.\n const c = makeNodeIdCounter(state.nodeSeq)\n const partner =\n action.subject.kind === 'node'\n ? { kind: 'node' as const, nodeId: action.subject.nodeId }\n : { kind: 'subtree' as const, node: cloneWithNewIds(action.subject.node, c.genId) }\n 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 // EC-287: nothing persists a graph — committed edits are lifted into the CMS\n // document by the composition-field editor's `onChange`. `useReducer` drives the\n // pure `builderReducer` (consecutive dispatches in one gesture apply in sequence);\n // the store below decorates the state with the derived undo/redo + selection flags.\n const [state, dispatch] = useReducer(builderReducer, composition, initialBuilderState)\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"]}
|
|
@@ -5,11 +5,12 @@ import { cloneWithNewIds } from '@elytracms/core/project-graph';
|
|
|
5
5
|
import { buildFragmentFromComposition } from '@elytracms/core/clipboard';
|
|
6
6
|
import { readFragmentFromClipboard, readFragmentFromDataTransfer, resolvePasteTarget, diffPastedSubtree, pasteSummaryMessage, writeFragmentToClipboard, writeFragmentToDataTransfer, } from '@elytracms/studio/lib/clipboard';
|
|
7
7
|
import { getStudioRegistry } from '@elytracms/studio/lib/registry';
|
|
8
|
+
import { shortId } from '@elytracms/studio/lib/persona/labels';
|
|
8
9
|
import { translate } from '@elytracms/studio/lib/i18n';
|
|
9
10
|
import { useData } from '@elytracms/studio/lib/data';
|
|
10
11
|
import { useCollab } from '../collab/collab-context';
|
|
11
12
|
import { Badge, Row, Text } from '@elytracms/studio/ui';
|
|
12
|
-
import { useBuilder } from './builder-store';
|
|
13
|
+
import { makeNodeIdCounter, useBuilder } from './builder-store';
|
|
13
14
|
let currentNotice = null;
|
|
14
15
|
const noticeListeners = new Set();
|
|
15
16
|
export function publishClipboardNotice(notice) {
|
|
@@ -46,17 +47,6 @@ export function clipboardEventBlocked(target) {
|
|
|
46
47
|
return true;
|
|
47
48
|
return el.isContentEditable === true;
|
|
48
49
|
}
|
|
49
|
-
function shortId(componentId) {
|
|
50
|
-
return componentId.replace('base.', '').replace('project.', '');
|
|
51
|
-
}
|
|
52
|
-
/** Mirrors the builder reducer's fresh-node-id counter (`node-<n>`). */
|
|
53
|
-
function counterFrom(start) {
|
|
54
|
-
let n = start;
|
|
55
|
-
return () => {
|
|
56
|
-
n += 1;
|
|
57
|
-
return `node-${n}`;
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
50
|
export function useStudioClipboard() {
|
|
61
51
|
const builder = useBuilder();
|
|
62
52
|
const data = useData();
|
|
@@ -107,8 +97,9 @@ export function useStudioClipboard() {
|
|
|
107
97
|
getManifest: (id) => studio.registry.getManifest(id),
|
|
108
98
|
});
|
|
109
99
|
// Mirror the reducer's deterministic re-id so the issue deep links point
|
|
110
|
-
// at exactly the node ids the inserted subtree will carry
|
|
111
|
-
|
|
100
|
+
// at exactly the node ids the inserted subtree will carry — the SAME counter
|
|
101
|
+
// the reducer uses (`makeNodeIdCounter`), so the mirror can never diverge.
|
|
102
|
+
const inserted = cloneWithNewIds(result.fragment.payload.root, makeNodeIdCounter(b.nodeSeq).genId);
|
|
112
103
|
// ONE dispatch: one history entry, one graph.insertNode command, one undo.
|
|
113
104
|
b.dispatch({ type: 'insert', target, node: result.fragment.payload.root });
|
|
114
105
|
const diff = diffPastedSubtree({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clipboard-actions.js","sourceRoot":"","sources":["../../../src/components/builder/clipboard-actions.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useRef, useSyncExternalStore } from 'react'\nimport { AlertTriangle, CircleCheck, Crosshair, X } from 'lucide-react'\nimport { cloneWithNewIds } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { buildFragmentFromComposition } from '@elytracms/core/clipboard'\nimport type { ClipboardFragment, ParseFragmentResult } from '@elytracms/core/clipboard'\nimport {\n readFragmentFromClipboard,\n readFragmentFromDataTransfer,\n resolvePasteTarget,\n diffPastedSubtree,\n pasteSummaryMessage,\n writeFragmentToClipboard,\n writeFragmentToDataTransfer,\n} from '@elytracms/studio/lib/clipboard'\nimport type { AsyncClipboard, PasteIssue } from '@elytracms/studio/lib/clipboard'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { useData } from '@elytracms/studio/lib/data'\nimport { useCollab } from '../collab/collab-context'\nimport { Badge, Row, Text } from '@elytracms/studio/ui'\nimport { useBuilder } from './builder-store'\n\n/**\n * Copy/cut/paste everywhere (EC-175, vision 3.3/3.7).\n *\n * One hook ({@link useStudioClipboard}) backs every affordance — canvas\n * keyboard shortcuts (wired inside the iframe by the canvas), page-tree\n * shortcuts and row buttons, and the blocks panel's \"copy exact\n * configuration\" — so they all produce/consume the same EC-174 fragment via\n * the OS clipboard (`lib/clipboard/os-clipboard`), never an app-internal\n * buffer. That is what makes paste work across documents, pages, projects,\n * and browser tabs.\n *\n * Dispatch contract: a paste is exactly ONE `insert` action (the fragment\n * root is a single subtree) → one history entry → one mirrored\n * `graph.insertNode` command — one undo step removes everything the paste\n * inserted. A cut is copy + exactly one `remove`; one undo restores it.\n *\n * Paste is PERMISSIVE: missing components and unresolvable bindings insert\n * anyway (the renderer's fallbacks keep them visible) and surface in a\n * notice — \"Pasted — 2 bindings unresolved, 1 component missing\" — whose\n * issues deep-link to the inserted nodes. Only structurally invalid clipboard\n * content is reported as a non-blocking error, never a crash or silent no-op.\n */\n\n// ---------------------------------------------------------------------------\n// Notice store — module-level pub/sub (the drag-session pattern) so any\n// surface can publish and the canvas overlay renders it.\n// ---------------------------------------------------------------------------\n\nexport type ClipboardNotice =\n | { kind: 'copied'; label: string }\n | { kind: 'cut'; label: string }\n | { kind: 'pasted'; summary: string; destinationLabel: string; issues: PasteIssue[] }\n | { kind: 'error'; message: string }\n\nlet currentNotice: ClipboardNotice | null = null\nconst noticeListeners = new Set<() => void>()\n\nexport function publishClipboardNotice(notice: ClipboardNotice | null): void {\n currentNotice = notice\n for (const listener of [...noticeListeners]) listener()\n}\n\nfunction subscribeNotice(listener: () => void): () => void {\n noticeListeners.add(listener)\n return () => noticeListeners.delete(listener)\n}\n\nfunction getNotice(): ClipboardNotice | null {\n return currentNotice\n}\n\nexport function useClipboardNotice(): ClipboardNotice | null {\n return useSyncExternalStore(subscribeNotice, getNotice, getNotice)\n}\n\n// ---------------------------------------------------------------------------\n// Shortcut scoping\n// ---------------------------------------------------------------------------\n\n/**\n * Whether a clipboard shortcut/event must stay inert for this target: form\n * fields and any contenteditable (which includes the EC-158/EC-159 inline\n * editors) own their own native clipboard behaviour.\n */\nexport function clipboardEventBlocked(target: EventTarget | null): boolean {\n const el = target as HTMLElement | null\n if (!el) return false\n const tag = el.tagName\n if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true\n if (typeof el.closest === 'function' && el.closest('[contenteditable]')) return true\n return el.isContentEditable === true\n}\n\n// ---------------------------------------------------------------------------\n// The clipboard surface\n// ---------------------------------------------------------------------------\n\nexport interface StudioClipboard {\n /**\n * Copy a canvas/tree node's subtree. Writes synchronously when a\n * ClipboardEvent's DataTransfer is given, else via the async Clipboard API\n * (`asyncClipboard` lets the canvas pass the iframe's own navigator —\n * Chromium scopes clipboard access to the *focused* document).\n */\n copyNode(nodeId: string, dt?: DataTransfer | null, asyncClipboard?: AsyncClipboard): void\n /** Copy, then remove the source in exactly one dispatch (one undo restores). */\n cutNode(nodeId: string, dt?: DataTransfer | null, asyncClipboard?: AsyncClipboard): void\n /** Copy a prebuilt fragment (blocks-panel \"copy exact configuration\"). */\n copyFragment(fragment: ClipboardFragment, label: string): void\n /** Paste from a paste event's DataTransfer (no permissions needed). */\n pasteFromDataTransfer(dt: DataTransfer, anchorId?: string | null): void\n /** Paste via the async Clipboard API (explicit affordances + shortcuts). */\n pasteFromClipboard(anchorId?: string | null, asyncClipboard?: AsyncClipboard): Promise<void>\n}\n\nfunction shortId(componentId: string): string {\n return componentId.replace('base.', '').replace('project.', '')\n}\n\n/** Mirrors the builder reducer's fresh-node-id counter (`node-<n>`). */\nfunction counterFrom(start: number): () => string {\n let n = start\n return () => {\n n += 1\n return `node-${n}`\n }\n}\n\nexport function useStudioClipboard(): StudioClipboard {\n const builder = useBuilder()\n const data = useData()\n const collab = useCollab()\n const studio = getStudioRegistry()\n\n // Event listeners outlive renders; refs keep them on the current state.\n const stateRef = useRef({ builder, data, projectId: collab.projectId })\n stateRef.current = { builder, data, projectId: collab.projectId }\n\n return useMemo<StudioClipboard>(() => {\n const fragmentFor = (nodeId: string): { fragment: ClipboardFragment; label: string } | null => {\n const { builder: b, projectId } = stateRef.current\n const built = buildFragmentFromComposition(b.present, nodeId, { source: { projectId } })\n if (!built.ok) {\n publishClipboardNotice({ kind: 'error', message: built.error.message })\n return null\n }\n return { fragment: built.fragment, label: shortId(built.fragment.payload.root.componentId) }\n }\n\n const write = (\n fragment: ClipboardFragment,\n label: string,\n kind: 'copied' | 'cut',\n dt?: DataTransfer | null,\n asyncClipboard?: AsyncClipboard,\n ): void => {\n if (dt) {\n writeFragmentToDataTransfer(dt, fragment)\n publishClipboardNotice({ kind, label })\n return\n }\n writeFragmentToClipboard(asyncClipboard ?? navigator.clipboard, fragment)\n .then(() => publishClipboardNotice({ kind, label }))\n .catch((cause: unknown) => {\n const detail = cause instanceof Error ? cause.message : String(cause)\n publishClipboardNotice({ kind: 'error', message: translate('builder.clipboard.writeError', { detail }) })\n })\n }\n\n const currentRoot = (): ComponentNode | undefined => {\n const { builder: b } = stateRef.current\n return b.present\n }\n\n const applyPaste = (result: ParseFragmentResult, anchorId?: string | null): void => {\n const { builder: b, data: d } = stateRef.current\n if (!result.ok) {\n publishClipboardNotice({ kind: 'error', message: result.error.message })\n return\n }\n const root = currentRoot()\n if (!root) return\n const { target, destinationLabel } = resolvePasteTarget({\n root,\n anchorId: anchorId !== undefined ? anchorId : b.selectedId,\n getManifest: (id) => studio.registry.getManifest(id),\n })\n // Mirror the reducer's deterministic re-id so the issue deep links point\n // at exactly the node ids the inserted subtree will carry.\n const inserted = cloneWithNewIds(result.fragment.payload.root, counterFrom(b.nodeSeq))\n // ONE dispatch: one history entry, one graph.insertNode command, one undo.\n b.dispatch({ type: 'insert', target, node: result.fragment.payload.root })\n const diff = diffPastedSubtree({\n node: inserted,\n hasComponent: (id) => studio.registry.getManifest(id) !== undefined,\n knownSourceIds: d.knownSourceIds,\n })\n publishClipboardNotice({\n kind: 'pasted',\n summary: pasteSummaryMessage(diff),\n destinationLabel,\n issues: diff.issues,\n })\n }\n\n return {\n copyNode(nodeId, dt, asyncClipboard) {\n const built = fragmentFor(nodeId)\n if (built) write(built.fragment, built.label, 'copied', dt, asyncClipboard)\n },\n cutNode(nodeId, dt, asyncClipboard) {\n const { builder: b } = stateRef.current\n const root = currentRoot()\n if (root && nodeId === root.id) {\n publishClipboardNotice({ kind: 'error', message: translate('builder.clipboard.rootCannotBeCut') })\n return\n }\n const built = fragmentFor(nodeId)\n if (!built) return\n write(built.fragment, built.label, 'cut', dt, asyncClipboard)\n // Exactly one structural dispatch: a single undo restores the cut node.\n b.dispatch({ type: 'remove', id: nodeId })\n },\n copyFragment(fragment, label) {\n write(fragment, label, 'copied')\n },\n pasteFromDataTransfer(dt, anchorId) {\n applyPaste(readFragmentFromDataTransfer(dt), anchorId)\n },\n async pasteFromClipboard(anchorId, asyncClipboard) {\n applyPaste(await readFragmentFromClipboard(asyncClipboard ?? navigator.clipboard), anchorId)\n },\n }\n }, [studio])\n}\n\n// ---------------------------------------------------------------------------\n// Listener wiring — one implementation for the page-tree window and the\n// canvas iframe document (events never cross the frame boundary).\n// ---------------------------------------------------------------------------\n\nexport interface ClipboardListenerOptions {\n clipboard: StudioClipboard\n /** Current selection: the copy/cut subject and the paste anchor. */\n getSelectedId: () => string | null\n /** Extra guard (e.g. the canvas's live inline-edit session). */\n isBlocked?: (target: EventTarget | null) => boolean\n /** The async Clipboard API of the listening document's own window. */\n asyncClipboard?: AsyncClipboard\n /** The listening document's selection — real text selections stay native. */\n getSelection?: () => Selection | null\n}\n\ninterface ListenerTarget {\n addEventListener(type: string, listener: (e: Event) => void): void\n removeEventListener(type: string, listener: (e: Event) => void): void\n}\n\n/**\n * Wire Cmd/Ctrl+C/X/V plus native copy/cut/paste ClipboardEvents.\n *\n * Shortcuts are handled on `keydown` (a selected node has no text selection,\n * so the browser would not fire native clipboard events for it); the\n * `preventDefault` there suppresses the native event, so the ClipboardEvent\n * listeners only ever see *other* triggers — context-menu copy/paste and\n * synthetic events — and the two paths can never double-handle one gesture.\n * Everything stays inert in form fields and inside the inline editors.\n */\nexport function attachClipboardListeners(target: ListenerTarget, opts: ClipboardListenerOptions): () => void {\n const blocked = (t: EventTarget | null): boolean =>\n clipboardEventBlocked(t) || (opts.isBlocked?.(t) ?? false)\n const hasTextSelection = (): boolean => {\n const selection = opts.getSelection?.()\n return selection !== null && selection !== undefined && !selection.isCollapsed\n }\n\n const onKeyDown = (e: Event) => {\n const event = e as KeyboardEvent\n if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) return\n const key = event.key.toLowerCase()\n if (key !== 'c' && key !== 'x' && key !== 'v') return\n if (blocked(event.target)) return\n if ((key === 'c' || key === 'x') && hasTextSelection()) return\n const selectedId = opts.getSelectedId()\n if (key === 'v') {\n event.preventDefault()\n void opts.clipboard.pasteFromClipboard(selectedId, opts.asyncClipboard)\n return\n }\n if (!selectedId) return\n event.preventDefault()\n if (key === 'c') opts.clipboard.copyNode(selectedId, null, opts.asyncClipboard)\n else opts.clipboard.cutNode(selectedId, null, opts.asyncClipboard)\n }\n\n const onCopyOrCut = (e: Event) => {\n const event = e as ClipboardEvent\n if (blocked(event.target) || hasTextSelection()) return\n const selectedId = opts.getSelectedId()\n if (!selectedId || !event.clipboardData) return\n event.preventDefault()\n if (event.type === 'copy') opts.clipboard.copyNode(selectedId, event.clipboardData)\n else opts.clipboard.cutNode(selectedId, event.clipboardData)\n }\n\n const onPaste = (e: Event) => {\n const event = e as ClipboardEvent\n if (blocked(event.target) || !event.clipboardData) return\n event.preventDefault()\n opts.clipboard.pasteFromDataTransfer(event.clipboardData, opts.getSelectedId())\n }\n\n target.addEventListener('keydown', onKeyDown)\n target.addEventListener('copy', onCopyOrCut)\n target.addEventListener('cut', onCopyOrCut)\n target.addEventListener('paste', onPaste)\n return () => {\n target.removeEventListener('keydown', onKeyDown)\n target.removeEventListener('copy', onCopyOrCut)\n target.removeEventListener('cut', onCopyOrCut)\n target.removeEventListener('paste', onPaste)\n }\n}\n\n// ---------------------------------------------------------------------------\n// Notice overlay — rendered by the canvas; issues deep-link to their nodes.\n// ---------------------------------------------------------------------------\n\nexport function ClipboardNoticeOverlay() {\n const notice = useClipboardNotice()\n const builder = useBuilder()\n if (!notice) return null\n\n const tone =\n notice.kind === 'error' ? 'error' : notice.kind === 'pasted' && notice.issues.length > 0 ? 'warning' : 'success'\n\n return (\n <div\n data-testid=\"clipboard-notice\"\n data-notice-kind={notice.kind}\n className=\"absolute bottom-4 left-1/2 z-20 w-max max-w-[min(420px,90%)] -translate-x-1/2\"\n >\n <div className=\"rounded-lg border border-border bg-popover p-2 text-popover-foreground shadow-lg\">\n <Row gap=\"2\" align=\"start\">\n {tone === 'error' ? (\n <AlertTriangle className=\"mt-0.5 size-3.5 shrink-0 text-destructive\" aria-hidden />\n ) : tone === 'warning' ? (\n <AlertTriangle className=\"mt-0.5 size-3.5 shrink-0 text-warning\" aria-hidden />\n ) : (\n <CircleCheck className=\"mt-0.5 size-3.5 shrink-0 text-success\" aria-hidden />\n )}\n <Text size=\"xs\" className=\"min-w-0 flex-1\">\n {notice.kind === 'copied'\n ? translate('builder.clipboard.copied', { label: notice.label })\n : notice.kind === 'cut'\n ? translate('builder.clipboard.cutNotice', { label: notice.label })\n : notice.kind === 'pasted'\n ? translate('builder.clipboard.pastedInto', {\n summary: notice.summary,\n destination: notice.destinationLabel,\n })\n : notice.message}\n </Text>\n <button\n type=\"button\"\n aria-label={translate('common.dismiss')}\n onClick={() => publishClipboardNotice(null)}\n className=\"shrink-0 rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground\"\n >\n <X className=\"size-3.5\" />\n </button>\n </Row>\n {notice.kind === 'pasted' && notice.issues.length > 0 ? (\n <div className=\"mt-1.5 flex flex-col gap-1\">\n {notice.issues.map((issue, i) => (\n <button\n key={`${issue.nodeId}-${i}`}\n type=\"button\"\n data-testid=\"clipboard-notice-issue\"\n onClick={() => builder.dispatch({ type: 'select', id: issue.nodeId })}\n className=\"flex w-full items-center gap-2 rounded-md p-1 text-left transition-colors hover:bg-muted\"\n >\n <Badge variant=\"warning\" className=\"shrink-0\">\n {issue.kind === 'missing-component'\n ? translate('builder.clipboard.issueComponent')\n : translate('builder.clipboard.issueBinding')}\n </Badge>\n <Text size=\"xs\" tone=\"muted\" className=\"min-w-0 flex-1 truncate\">\n {issue.message}\n </Text>\n <Crosshair className=\"size-3.5 shrink-0 text-muted-foreground\" aria-hidden />\n </button>\n ))}\n </div>\n ) : null}\n </div>\n </div>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"clipboard-actions.js","sourceRoot":"","sources":["../../../src/components/builder/clipboard-actions.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useRef, useSyncExternalStore } from 'react'\nimport { AlertTriangle, CircleCheck, Crosshair, X } from 'lucide-react'\nimport { cloneWithNewIds } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { buildFragmentFromComposition } from '@elytracms/core/clipboard'\nimport type { ClipboardFragment, ParseFragmentResult } from '@elytracms/core/clipboard'\nimport {\n readFragmentFromClipboard,\n readFragmentFromDataTransfer,\n resolvePasteTarget,\n diffPastedSubtree,\n pasteSummaryMessage,\n writeFragmentToClipboard,\n writeFragmentToDataTransfer,\n} from '@elytracms/studio/lib/clipboard'\nimport type { AsyncClipboard, PasteIssue } from '@elytracms/studio/lib/clipboard'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { shortId } from '@elytracms/studio/lib/persona/labels'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { useData } from '@elytracms/studio/lib/data'\nimport { useCollab } from '../collab/collab-context'\nimport { Badge, Row, Text } from '@elytracms/studio/ui'\nimport { makeNodeIdCounter, useBuilder } from './builder-store'\n\n/**\n * Copy/cut/paste everywhere (EC-175, vision 3.3/3.7).\n *\n * One hook ({@link useStudioClipboard}) backs every affordance — canvas\n * keyboard shortcuts (wired inside the iframe by the canvas), page-tree\n * shortcuts and row buttons, and the blocks panel's \"copy exact\n * configuration\" — so they all produce/consume the same EC-174 fragment via\n * the OS clipboard (`lib/clipboard/os-clipboard`), never an app-internal\n * buffer. That is what makes paste work across documents, pages, projects,\n * and browser tabs.\n *\n * Dispatch contract: a paste is exactly ONE `insert` action (the fragment\n * root is a single subtree) → one history entry → one mirrored\n * `graph.insertNode` command — one undo step removes everything the paste\n * inserted. A cut is copy + exactly one `remove`; one undo restores it.\n *\n * Paste is PERMISSIVE: missing components and unresolvable bindings insert\n * anyway (the renderer's fallbacks keep them visible) and surface in a\n * notice — \"Pasted — 2 bindings unresolved, 1 component missing\" — whose\n * issues deep-link to the inserted nodes. Only structurally invalid clipboard\n * content is reported as a non-blocking error, never a crash or silent no-op.\n */\n\n// ---------------------------------------------------------------------------\n// Notice store — module-level pub/sub (the drag-session pattern) so any\n// surface can publish and the canvas overlay renders it.\n// ---------------------------------------------------------------------------\n\nexport type ClipboardNotice =\n | { kind: 'copied'; label: string }\n | { kind: 'cut'; label: string }\n | { kind: 'pasted'; summary: string; destinationLabel: string; issues: PasteIssue[] }\n | { kind: 'error'; message: string }\n\nlet currentNotice: ClipboardNotice | null = null\nconst noticeListeners = new Set<() => void>()\n\nexport function publishClipboardNotice(notice: ClipboardNotice | null): void {\n currentNotice = notice\n for (const listener of [...noticeListeners]) listener()\n}\n\nfunction subscribeNotice(listener: () => void): () => void {\n noticeListeners.add(listener)\n return () => noticeListeners.delete(listener)\n}\n\nfunction getNotice(): ClipboardNotice | null {\n return currentNotice\n}\n\nexport function useClipboardNotice(): ClipboardNotice | null {\n return useSyncExternalStore(subscribeNotice, getNotice, getNotice)\n}\n\n// ---------------------------------------------------------------------------\n// Shortcut scoping\n// ---------------------------------------------------------------------------\n\n/**\n * Whether a clipboard shortcut/event must stay inert for this target: form\n * fields and any contenteditable (which includes the EC-158/EC-159 inline\n * editors) own their own native clipboard behaviour.\n */\nexport function clipboardEventBlocked(target: EventTarget | null): boolean {\n const el = target as HTMLElement | null\n if (!el) return false\n const tag = el.tagName\n if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true\n if (typeof el.closest === 'function' && el.closest('[contenteditable]')) return true\n return el.isContentEditable === true\n}\n\n// ---------------------------------------------------------------------------\n// The clipboard surface\n// ---------------------------------------------------------------------------\n\nexport interface StudioClipboard {\n /**\n * Copy a canvas/tree node's subtree. Writes synchronously when a\n * ClipboardEvent's DataTransfer is given, else via the async Clipboard API\n * (`asyncClipboard` lets the canvas pass the iframe's own navigator —\n * Chromium scopes clipboard access to the *focused* document).\n */\n copyNode(nodeId: string, dt?: DataTransfer | null, asyncClipboard?: AsyncClipboard): void\n /** Copy, then remove the source in exactly one dispatch (one undo restores). */\n cutNode(nodeId: string, dt?: DataTransfer | null, asyncClipboard?: AsyncClipboard): void\n /** Copy a prebuilt fragment (blocks-panel \"copy exact configuration\"). */\n copyFragment(fragment: ClipboardFragment, label: string): void\n /** Paste from a paste event's DataTransfer (no permissions needed). */\n pasteFromDataTransfer(dt: DataTransfer, anchorId?: string | null): void\n /** Paste via the async Clipboard API (explicit affordances + shortcuts). */\n pasteFromClipboard(anchorId?: string | null, asyncClipboard?: AsyncClipboard): Promise<void>\n}\n\nexport function useStudioClipboard(): StudioClipboard {\n const builder = useBuilder()\n const data = useData()\n const collab = useCollab()\n const studio = getStudioRegistry()\n\n // Event listeners outlive renders; refs keep them on the current state.\n const stateRef = useRef({ builder, data, projectId: collab.projectId })\n stateRef.current = { builder, data, projectId: collab.projectId }\n\n return useMemo<StudioClipboard>(() => {\n const fragmentFor = (nodeId: string): { fragment: ClipboardFragment; label: string } | null => {\n const { builder: b, projectId } = stateRef.current\n const built = buildFragmentFromComposition(b.present, nodeId, { source: { projectId } })\n if (!built.ok) {\n publishClipboardNotice({ kind: 'error', message: built.error.message })\n return null\n }\n return { fragment: built.fragment, label: shortId(built.fragment.payload.root.componentId) }\n }\n\n const write = (\n fragment: ClipboardFragment,\n label: string,\n kind: 'copied' | 'cut',\n dt?: DataTransfer | null,\n asyncClipboard?: AsyncClipboard,\n ): void => {\n if (dt) {\n writeFragmentToDataTransfer(dt, fragment)\n publishClipboardNotice({ kind, label })\n return\n }\n writeFragmentToClipboard(asyncClipboard ?? navigator.clipboard, fragment)\n .then(() => publishClipboardNotice({ kind, label }))\n .catch((cause: unknown) => {\n const detail = cause instanceof Error ? cause.message : String(cause)\n publishClipboardNotice({ kind: 'error', message: translate('builder.clipboard.writeError', { detail }) })\n })\n }\n\n const currentRoot = (): ComponentNode | undefined => {\n const { builder: b } = stateRef.current\n return b.present\n }\n\n const applyPaste = (result: ParseFragmentResult, anchorId?: string | null): void => {\n const { builder: b, data: d } = stateRef.current\n if (!result.ok) {\n publishClipboardNotice({ kind: 'error', message: result.error.message })\n return\n }\n const root = currentRoot()\n if (!root) return\n const { target, destinationLabel } = resolvePasteTarget({\n root,\n anchorId: anchorId !== undefined ? anchorId : b.selectedId,\n getManifest: (id) => studio.registry.getManifest(id),\n })\n // Mirror the reducer's deterministic re-id so the issue deep links point\n // at exactly the node ids the inserted subtree will carry — the SAME counter\n // the reducer uses (`makeNodeIdCounter`), so the mirror can never diverge.\n const inserted = cloneWithNewIds(result.fragment.payload.root, makeNodeIdCounter(b.nodeSeq).genId)\n // ONE dispatch: one history entry, one graph.insertNode command, one undo.\n b.dispatch({ type: 'insert', target, node: result.fragment.payload.root })\n const diff = diffPastedSubtree({\n node: inserted,\n hasComponent: (id) => studio.registry.getManifest(id) !== undefined,\n knownSourceIds: d.knownSourceIds,\n })\n publishClipboardNotice({\n kind: 'pasted',\n summary: pasteSummaryMessage(diff),\n destinationLabel,\n issues: diff.issues,\n })\n }\n\n return {\n copyNode(nodeId, dt, asyncClipboard) {\n const built = fragmentFor(nodeId)\n if (built) write(built.fragment, built.label, 'copied', dt, asyncClipboard)\n },\n cutNode(nodeId, dt, asyncClipboard) {\n const { builder: b } = stateRef.current\n const root = currentRoot()\n if (root && nodeId === root.id) {\n publishClipboardNotice({ kind: 'error', message: translate('builder.clipboard.rootCannotBeCut') })\n return\n }\n const built = fragmentFor(nodeId)\n if (!built) return\n write(built.fragment, built.label, 'cut', dt, asyncClipboard)\n // Exactly one structural dispatch: a single undo restores the cut node.\n b.dispatch({ type: 'remove', id: nodeId })\n },\n copyFragment(fragment, label) {\n write(fragment, label, 'copied')\n },\n pasteFromDataTransfer(dt, anchorId) {\n applyPaste(readFragmentFromDataTransfer(dt), anchorId)\n },\n async pasteFromClipboard(anchorId, asyncClipboard) {\n applyPaste(await readFragmentFromClipboard(asyncClipboard ?? navigator.clipboard), anchorId)\n },\n }\n }, [studio])\n}\n\n// ---------------------------------------------------------------------------\n// Listener wiring — one implementation for the page-tree window and the\n// canvas iframe document (events never cross the frame boundary).\n// ---------------------------------------------------------------------------\n\nexport interface ClipboardListenerOptions {\n clipboard: StudioClipboard\n /** Current selection: the copy/cut subject and the paste anchor. */\n getSelectedId: () => string | null\n /** Extra guard (e.g. the canvas's live inline-edit session). */\n isBlocked?: (target: EventTarget | null) => boolean\n /** The async Clipboard API of the listening document's own window. */\n asyncClipboard?: AsyncClipboard\n /** The listening document's selection — real text selections stay native. */\n getSelection?: () => Selection | null\n}\n\ninterface ListenerTarget {\n addEventListener(type: string, listener: (e: Event) => void): void\n removeEventListener(type: string, listener: (e: Event) => void): void\n}\n\n/**\n * Wire Cmd/Ctrl+C/X/V plus native copy/cut/paste ClipboardEvents.\n *\n * Shortcuts are handled on `keydown` (a selected node has no text selection,\n * so the browser would not fire native clipboard events for it); the\n * `preventDefault` there suppresses the native event, so the ClipboardEvent\n * listeners only ever see *other* triggers — context-menu copy/paste and\n * synthetic events — and the two paths can never double-handle one gesture.\n * Everything stays inert in form fields and inside the inline editors.\n */\nexport function attachClipboardListeners(target: ListenerTarget, opts: ClipboardListenerOptions): () => void {\n const blocked = (t: EventTarget | null): boolean =>\n clipboardEventBlocked(t) || (opts.isBlocked?.(t) ?? false)\n const hasTextSelection = (): boolean => {\n const selection = opts.getSelection?.()\n return selection !== null && selection !== undefined && !selection.isCollapsed\n }\n\n const onKeyDown = (e: Event) => {\n const event = e as KeyboardEvent\n if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) return\n const key = event.key.toLowerCase()\n if (key !== 'c' && key !== 'x' && key !== 'v') return\n if (blocked(event.target)) return\n if ((key === 'c' || key === 'x') && hasTextSelection()) return\n const selectedId = opts.getSelectedId()\n if (key === 'v') {\n event.preventDefault()\n void opts.clipboard.pasteFromClipboard(selectedId, opts.asyncClipboard)\n return\n }\n if (!selectedId) return\n event.preventDefault()\n if (key === 'c') opts.clipboard.copyNode(selectedId, null, opts.asyncClipboard)\n else opts.clipboard.cutNode(selectedId, null, opts.asyncClipboard)\n }\n\n const onCopyOrCut = (e: Event) => {\n const event = e as ClipboardEvent\n if (blocked(event.target) || hasTextSelection()) return\n const selectedId = opts.getSelectedId()\n if (!selectedId || !event.clipboardData) return\n event.preventDefault()\n if (event.type === 'copy') opts.clipboard.copyNode(selectedId, event.clipboardData)\n else opts.clipboard.cutNode(selectedId, event.clipboardData)\n }\n\n const onPaste = (e: Event) => {\n const event = e as ClipboardEvent\n if (blocked(event.target) || !event.clipboardData) return\n event.preventDefault()\n opts.clipboard.pasteFromDataTransfer(event.clipboardData, opts.getSelectedId())\n }\n\n target.addEventListener('keydown', onKeyDown)\n target.addEventListener('copy', onCopyOrCut)\n target.addEventListener('cut', onCopyOrCut)\n target.addEventListener('paste', onPaste)\n return () => {\n target.removeEventListener('keydown', onKeyDown)\n target.removeEventListener('copy', onCopyOrCut)\n target.removeEventListener('cut', onCopyOrCut)\n target.removeEventListener('paste', onPaste)\n }\n}\n\n// ---------------------------------------------------------------------------\n// Notice overlay — rendered by the canvas; issues deep-link to their nodes.\n// ---------------------------------------------------------------------------\n\nexport function ClipboardNoticeOverlay() {\n const notice = useClipboardNotice()\n const builder = useBuilder()\n if (!notice) return null\n\n const tone =\n notice.kind === 'error' ? 'error' : notice.kind === 'pasted' && notice.issues.length > 0 ? 'warning' : 'success'\n\n return (\n <div\n data-testid=\"clipboard-notice\"\n data-notice-kind={notice.kind}\n className=\"absolute bottom-4 left-1/2 z-20 w-max max-w-[min(420px,90%)] -translate-x-1/2\"\n >\n <div className=\"rounded-lg border border-border bg-popover p-2 text-popover-foreground shadow-lg\">\n <Row gap=\"2\" align=\"start\">\n {tone === 'error' ? (\n <AlertTriangle className=\"mt-0.5 size-3.5 shrink-0 text-destructive\" aria-hidden />\n ) : tone === 'warning' ? (\n <AlertTriangle className=\"mt-0.5 size-3.5 shrink-0 text-warning\" aria-hidden />\n ) : (\n <CircleCheck className=\"mt-0.5 size-3.5 shrink-0 text-success\" aria-hidden />\n )}\n <Text size=\"xs\" className=\"min-w-0 flex-1\">\n {notice.kind === 'copied'\n ? translate('builder.clipboard.copied', { label: notice.label })\n : notice.kind === 'cut'\n ? translate('builder.clipboard.cutNotice', { label: notice.label })\n : notice.kind === 'pasted'\n ? translate('builder.clipboard.pastedInto', {\n summary: notice.summary,\n destination: notice.destinationLabel,\n })\n : notice.message}\n </Text>\n <button\n type=\"button\"\n aria-label={translate('common.dismiss')}\n onClick={() => publishClipboardNotice(null)}\n className=\"shrink-0 rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground\"\n >\n <X className=\"size-3.5\" />\n </button>\n </Row>\n {notice.kind === 'pasted' && notice.issues.length > 0 ? (\n <div className=\"mt-1.5 flex flex-col gap-1\">\n {notice.issues.map((issue, i) => (\n <button\n key={`${issue.nodeId}-${i}`}\n type=\"button\"\n data-testid=\"clipboard-notice-issue\"\n onClick={() => builder.dispatch({ type: 'select', id: issue.nodeId })}\n className=\"flex w-full items-center gap-2 rounded-md p-1 text-left transition-colors hover:bg-muted\"\n >\n <Badge variant=\"warning\" className=\"shrink-0\">\n {issue.kind === 'missing-component'\n ? translate('builder.clipboard.issueComponent')\n : translate('builder.clipboard.issueBinding')}\n </Badge>\n <Text size=\"xs\" tone=\"muted\" className=\"min-w-0 flex-1 truncate\">\n {issue.message}\n </Text>\n <Crosshair className=\"size-3.5 shrink-0 text-muted-foreground\" aria-hidden />\n </button>\n ))}\n </div>\n ) : null}\n </div>\n </div>\n )\n}\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Lazy boundary for the block-field page builder (EC-322 load-time pass).
|
|
4
|
+
*
|
|
5
|
+
* `CompositionFieldEditor` mounts the full page-builder canvas — the canvas,
|
|
6
|
+
* page tree, block inserter, property inspector, clipboard, drag/reorder and the
|
|
7
|
+
* builder store (the studio's single heaviest UI subtree). The detail editor
|
|
8
|
+
* imports it statically but only renders it for documents that actually carry a
|
|
9
|
+
* `blocks` field, so every ordinary document editor was paying the whole builder's
|
|
10
|
+
* download cost for a surface it never shows.
|
|
11
|
+
*
|
|
12
|
+
* Splitting it behind {@link lazy} keeps the builder out of the CMS detail-view
|
|
13
|
+
* critical path; it downloads on demand the first time a `blocks` field opens. The
|
|
14
|
+
* detail editor's seam is unchanged — it still imports `CompositionFieldEditor`,
|
|
15
|
+
* now the lazy-wrapped one, and renders it behind the same `canvasField` gate.
|
|
16
|
+
*/
|
|
17
|
+
declare const CompositionFieldEditorImpl: import("react").LazyExoticComponent<typeof import("./composition-field-editor").CompositionFieldEditor>;
|
|
18
|
+
export declare function CompositionFieldEditor(props: ComponentProps<typeof CompositionFieldEditorImpl>): import("react").JSX.Element;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Suspense, lazy } from 'react';
|
|
3
|
+
import { Text } from '@elytracms/studio/ui';
|
|
4
|
+
import { translate } from '@elytracms/studio/lib/i18n';
|
|
5
|
+
/**
|
|
6
|
+
* Lazy boundary for the block-field page builder (EC-322 load-time pass).
|
|
7
|
+
*
|
|
8
|
+
* `CompositionFieldEditor` mounts the full page-builder canvas — the canvas,
|
|
9
|
+
* page tree, block inserter, property inspector, clipboard, drag/reorder and the
|
|
10
|
+
* builder store (the studio's single heaviest UI subtree). The detail editor
|
|
11
|
+
* imports it statically but only renders it for documents that actually carry a
|
|
12
|
+
* `blocks` field, so every ordinary document editor was paying the whole builder's
|
|
13
|
+
* download cost for a surface it never shows.
|
|
14
|
+
*
|
|
15
|
+
* Splitting it behind {@link lazy} keeps the builder out of the CMS detail-view
|
|
16
|
+
* critical path; it downloads on demand the first time a `blocks` field opens. The
|
|
17
|
+
* detail editor's seam is unchanged — it still imports `CompositionFieldEditor`,
|
|
18
|
+
* now the lazy-wrapped one, and renders it behind the same `canvasField` gate.
|
|
19
|
+
*/
|
|
20
|
+
const CompositionFieldEditorImpl = lazy(() => import('./composition-field-editor').then((module) => ({
|
|
21
|
+
default: module.CompositionFieldEditor,
|
|
22
|
+
})));
|
|
23
|
+
function CanvasFallback() {
|
|
24
|
+
return (_jsx("div", { className: "grid min-h-0 flex-1 place-items-center p-8", children: _jsx(Text, { size: "sm", tone: "muted", children: translate('builder.canvas.loading') }) }));
|
|
25
|
+
}
|
|
26
|
+
export function CompositionFieldEditor(props) {
|
|
27
|
+
return (_jsx(Suspense, { fallback: _jsx(CanvasFallback, {}), children: _jsx(CompositionFieldEditorImpl, { ...props }) }));
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=composition-field-editor-lazy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"composition-field-editor-lazy.js","sourceRoot":"","sources":["../../../src/components/builder/composition-field-editor-lazy.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AAEtC,OAAO,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAEtD;;;;;;;;;;;;;;GAcG;AACH,MAAM,0BAA0B,GAAG,IAAI,CAAC,GAAG,EAAE,CAC3C,MAAM,CAAC,4BAA4B,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACrD,OAAO,EAAE,MAAM,CAAC,sBAAsB;CACvC,CAAC,CAAC,CACJ,CAAA;AAED,SAAS,cAAc;IACrB,OAAO,CACL,cAAK,SAAS,EAAC,4CAA4C,YACzD,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,SAAS,CAAC,wBAAwB,CAAC,GAC/B,GACH,CACP,CAAA;AACH,CAAC;AAED,MAAM,UAAU,sBAAsB,CACpC,KAAwD;IAExD,OAAO,CACL,KAAC,QAAQ,IAAC,QAAQ,EAAE,KAAC,cAAc,KAAG,YACpC,KAAC,0BAA0B,OAAK,KAAK,GAAI,GAChC,CACZ,CAAA;AACH,CAAC","sourcesContent":["import { Suspense, lazy } from 'react'\nimport type { ComponentProps } from 'react'\nimport { Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * Lazy boundary for the block-field page builder (EC-322 load-time pass).\n *\n * `CompositionFieldEditor` mounts the full page-builder canvas — the canvas,\n * page tree, block inserter, property inspector, clipboard, drag/reorder and the\n * builder store (the studio's single heaviest UI subtree). The detail editor\n * imports it statically but only renders it for documents that actually carry a\n * `blocks` field, so every ordinary document editor was paying the whole builder's\n * download cost for a surface it never shows.\n *\n * Splitting it behind {@link lazy} keeps the builder out of the CMS detail-view\n * critical path; it downloads on demand the first time a `blocks` field opens. The\n * detail editor's seam is unchanged — it still imports `CompositionFieldEditor`,\n * now the lazy-wrapped one, and renders it behind the same `canvasField` gate.\n */\nconst CompositionFieldEditorImpl = lazy(() =>\n import('./composition-field-editor').then((module) => ({\n default: module.CompositionFieldEditor,\n })),\n)\n\nfunction CanvasFallback() {\n return (\n <div className=\"grid min-h-0 flex-1 place-items-center p-8\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('builder.canvas.loading')}\n </Text>\n </div>\n )\n}\n\nexport function CompositionFieldEditor(\n props: ComponentProps<typeof CompositionFieldEditorImpl>,\n) {\n return (\n <Suspense fallback={<CanvasFallback />}>\n <CompositionFieldEditorImpl {...props} />\n </Suspense>\n )\n}\n"]}
|
|
@@ -13,7 +13,6 @@ import { PropertyInspector } from './property-inspector';
|
|
|
13
13
|
import { InspectorSidebar } from '../inspector/inspector-sidebar';
|
|
14
14
|
import { InsertMenu } from './insert-menu';
|
|
15
15
|
import { resolveInsertContext } from './insert-context';
|
|
16
|
-
import { CompositionScopeProvider } from './composition-scope';
|
|
17
16
|
import { ReuseProvider } from './reuse-context';
|
|
18
17
|
/**
|
|
19
18
|
* EC-188 — the block-field surface: one canvas engine mounted on a composition
|
|
@@ -29,8 +28,6 @@ import { ReuseProvider } from './reuse-context';
|
|
|
29
28
|
* the CMS detail editor so the page builder gets full screen real estate rather
|
|
30
29
|
* than being crammed into the field list.
|
|
31
30
|
*/
|
|
32
|
-
/** Stable scope value (a fresh object each render would re-render consumers). */
|
|
33
|
-
const COMPOSITION_SCOPE = { active: true };
|
|
34
31
|
const SYNTH_ROOT_ID = 'composition.root';
|
|
35
32
|
/** The implicit root container the field's blocks compose inside (like a page root). */
|
|
36
33
|
const SYNTH_ROOT_COMPONENT = 'base.Stack';
|
|
@@ -57,7 +54,7 @@ export function CompositionFieldEditor({ field, value, onChange, readOnly, trail
|
|
|
57
54
|
// (BuilderProvider reads `composition` only in its lazy initial state, so later
|
|
58
55
|
// prop changes are ignored regardless.)
|
|
59
56
|
const initialRoot = useMemo(() => buildSyntheticRoot(value), []);
|
|
60
|
-
return (_jsxs(BuilderProvider, { composition: initialRoot, children: [_jsx(CompositionLift, { multiple: field.multiple, onChange: onChange, readOnly: readOnly }), _jsx(ReuseProvider, { children: _jsx(
|
|
57
|
+
return (_jsxs(BuilderProvider, { composition: initialRoot, children: [_jsx(CompositionLift, { multiple: field.multiple, onChange: onChange, readOnly: readOnly }), _jsx(ReuseProvider, { children: _jsx(CompositionEditorShell, { readOnly: readOnly, fieldAllow: field.allow, trailingPanes: trailingPanes }) })] }));
|
|
61
58
|
}
|
|
62
59
|
/**
|
|
63
60
|
* Lifts committed edits out of the scoped store into the CMS draft. The first
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"composition-field-editor.js","sourceRoot":"","sources":["../../../src/components/builder/composition-field-editor.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useMemo, useRef, useState } from 'react'\nimport { AlertTriangle, Plus, Redo2, SlidersHorizontal, Undo2 } from 'lucide-react'\nimport { DEFAULT_SLOT, blockRootsOf } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport type { BlocksFieldDef } from '@elytracms/core/cms-core'\nimport { Button, Panel, Row, Spacer, Text, Toolbar, ToolbarGroup, ToolbarSeparator } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { BuilderProvider, useBuilder } from './builder-store'\nimport { useCanvasViewports } from './canvas-breakpoints'\nimport type { ViewportOption } from './canvas-breakpoints'\nimport { BuilderCanvas } from './canvas'\nimport { PageTree } from './page-tree'\nimport { PropertyInspector } from './property-inspector'\nimport { InspectorSidebar } from '../inspector/inspector-sidebar'\nimport type { InspectorPane } from '../inspector/inspector-sidebar'\nimport { InsertMenu } from './insert-menu'\nimport type { InsertContext } from './insert-menu'\nimport { resolveInsertContext } from './insert-context'\nimport { CompositionScopeProvider } from './composition-scope'\nimport { ReuseProvider } from './reuse-context'\n\n/**\n * EC-188 — the block-field surface: one canvas engine mounted on a composition\n * **field value** (AD-12), not the project graph.\n *\n * The composition tree is wrapped in a synthetic single-page graph and edited by\n * a SCOPED `BuilderProvider` with **no `projectId`** (so the graph persister is a\n * no-op — builder-store.tsx). Every committed edit is lifted out via `onChange`\n * into the CMS document draft; the document's own Save persists it (the tree is\n * stored as document data, `v.any()`). This reuses the real canvas + inspector +\n * blocks panel unchanged — the same EC-158–164 mechanics — without touching the\n * project-wide builder store. Surfaced on a dedicated \"Canvas\" fieldset tab in\n * the CMS detail editor so the page builder gets full screen real estate rather\n * than being crammed into the field list.\n */\n\n/** Stable scope value (a fresh object each render would re-render consumers). */\nconst COMPOSITION_SCOPE = { active: true } as const\n\nconst SYNTH_ROOT_ID = 'composition.root'\n/** The implicit root container the field's blocks compose inside (like a page root). */\nconst SYNTH_ROOT_COMPONENT = 'base.Stack'\n\n/**\n * Wrap the field value's blocks into a synthetic root container (EC-287). The\n * builder store edits this single root-bearing composition directly; its\n * children are the field's blocks, lifted back out via `onChange`.\n */\nfunction buildSyntheticRoot(value: unknown): ComponentNode {\n return {\n id: SYNTH_ROOT_ID,\n componentId: SYNTH_ROOT_COMPONENT,\n slots: { [DEFAULT_SLOT]: blockRootsOf(value) },\n }\n}\n\n/** Unwrap the synthetic root's children back to a field value per cardinality. */\nfunction unwrapValue(blocks: readonly ComponentNode[], multiple: boolean): unknown {\n return multiple ? [...blocks] : blocks[0]\n}\n\nexport function CompositionFieldEditor({\n field,\n value,\n onChange,\n readOnly,\n trailingPanes,\n}: {\n field: BlocksFieldDef\n value: unknown\n onChange: (value: unknown) => void\n readOnly: boolean\n /** Inspector panes the document editor appends after the canvas-supplied\n * `Properties` pane (EC-224 — the frame-owned `Versions` pane). */\n trailingPanes?: readonly InspectorPane[]\n}) {\n // Build the synthetic root ONCE from the value at mount. The editor is\n // uncontrolled thereafter — it is the source of truth while open and lifts\n // changes out via onChange; re-opening the tab remounts from the current draft.\n // (BuilderProvider reads `composition` only in its lazy initial state, so later\n // prop changes are ignored regardless.)\n const initialRoot = useMemo(() => buildSyntheticRoot(value), [])\n\n return (\n <BuilderProvider composition={initialRoot}>\n <CompositionLift multiple={field.multiple} onChange={onChange} readOnly={readOnly} />\n <ReuseProvider>\n <CompositionScopeProvider value={COMPOSITION_SCOPE}>\n <CompositionEditorShell\n readOnly={readOnly}\n fieldAllow={field.allow}\n trailingPanes={trailingPanes}\n />\n </CompositionScopeProvider>\n </ReuseProvider>\n </BuilderProvider>\n )\n}\n\n/**\n * Lifts committed edits out of the scoped store into the CMS draft. The first\n * render establishes the (container-normalized) baseline so simply opening the\n * tab never marks the document dirty; every subsequent change to the block list\n * emits the unwrapped value.\n */\nfunction CompositionLift({\n multiple,\n onChange,\n readOnly,\n}: {\n multiple: boolean\n onChange: (value: unknown) => void\n readOnly: boolean\n}) {\n const builder = useBuilder()\n const blocks = builder.present.slots?.[DEFAULT_SLOT] ?? EMPTY_BLOCKS\n const onChangeRef = useRef(onChange)\n onChangeRef.current = onChange\n const baselineRef = useRef<readonly ComponentNode[] | null>(null)\n\n useEffect(() => {\n // EC-226: never lift edits out of a READ-ONLY canvas (the Published view).\n // The canvas's keyboard listeners (clipboard paste/cut) aren't blocked by the\n // visual `pointer-events-none`, so without this a Cmd+V could mutate the\n // graph; refusing to lift keeps the document draft intact regardless. The\n // editor remounts per perspective, so any transient in-canvas change is\n // discarded on switch.\n if (readOnly) return\n if (baselineRef.current === null) {\n baselineRef.current = blocks\n return\n }\n if (blocks !== baselineRef.current) {\n baselineRef.current = blocks\n onChangeRef.current(unwrapValue(blocks, multiple))\n }\n }, [blocks, multiple, readOnly])\n\n return null\n}\n\nconst EMPTY_BLOCKS: readonly ComponentNode[] = []\n\n/** A breakpoint preview-width toggle (EC-211/EC-250). Switching the viewport\n * resizes only the preview frame (not an undoable edit, writes no graph command).\n * The label is the breakpoint name (`Full`/`sm`/`md`/…) with the real px width in\n * the tooltip — so the switcher reflects the site's actual screens, not a fixed\n * desktop/tablet/phone triple. */\nfunction ViewportButton({ option }: { option: ViewportOption }) {\n const builder = useBuilder()\n const active = builder.viewport === option.key\n return (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={translate('builder.canvas.viewportPreviewAria', { label: option.label })}\n aria-pressed={active}\n title={\n option.px !== undefined\n ? `${option.label} · ${option.px}px`\n : translate('builder.canvas.viewportWidthTitle', { label: option.label })\n }\n onClick={() => builder.dispatch({ type: 'viewport', viewport: option.key })}\n className={cn('px-2 text-xs', active && 'bg-accent text-accent-foreground')}\n >\n {option.label}\n </Button>\n )\n}\n\n/** The centered breakpoint switcher (EC-250): the resolved viewport set, plus a\n * visible validation issue when a project declared breakpoints but none are usable. */\nfunction BreakpointSwitcher() {\n const { viewports, issue } = useCanvasViewports()\n return (\n <Row gap=\"1\" align=\"center\">\n <ToolbarGroup>\n {viewports.map((v) => (\n <ViewportButton key={v.key} option={v} />\n ))}\n </ToolbarGroup>\n {issue ? (\n <Row gap=\"1\" align=\"center\" className=\"text-amber-600 dark:text-amber-400\" title={issue}>\n <AlertTriangle aria-hidden className=\"size-3.5\" />\n <Text size=\"xs\">{translate('builder.canvas.breakpointsIssueLabel')}</Text>\n </Row>\n ) : null}\n </Row>\n )\n}\n\n/**\n * The slim editor surface — the canvas + layers + inspector, plus Insert and\n * undo/redo. It deliberately drops the project-builder *lifecycle* chrome\n * (publish, versions, AI, page SEO): the document owns the lifecycle, not the\n * field. It keeps spatial/preview controls — including the breakpoint\n * preview-width switcher (EC-211), a canvas concern, not a lifecycle one.\n */\nfunction CompositionEditorShell({\n readOnly,\n fieldAllow,\n trailingPanes,\n}: {\n readOnly: boolean\n /** The field's top-level component vocabulary (EC-188) — the leash applied to\n * inserts at the composition root so forbidden components aren't even offered. */\n fieldAllow?: readonly string[]\n /** Document-level panes appended after `Properties` (EC-224). */\n trailingPanes?: readonly InspectorPane[]\n}) {\n const builder = useBuilder()\n const dispatch = builder.dispatch\n const [insertContext, setInsertContext] = useState<InsertContext | null>(null)\n const [insertOpen, setInsertOpen] = useState(false)\n const rootId = builder.present.id\n\n // The canvas SUPPLIES the `Properties` pane (EC-224); the document editor\n // appends its own (`Versions`). Properties dims under read-only just as the\n // whole inspector column used to — but the appended panes stay interactive.\n const propertiesPane = useMemo<InspectorPane>(\n () => ({\n id: 'properties',\n label: translate('builder.inspector.propertiesPane'),\n icon: <SlidersHorizontal className=\"size-3.5\" />,\n render: () => (\n <div\n className={cn(\n 'h-full min-h-0 overflow-hidden',\n readOnly && 'pointer-events-none opacity-60',\n )}\n >\n <PropertyInspector />\n </div>\n ),\n }),\n [readOnly],\n )\n const panes = useMemo<InspectorPane[]>(\n () => [propertiesPane, ...(trailingPanes ?? [])],\n [propertiesPane, trailingPanes],\n )\n\n function openInsert(nodeId: string) {\n const ctx = resolveInsertContext(builder, nodeId, fieldAllow)\n if (ctx) {\n setInsertContext(ctx)\n setInsertOpen(true)\n }\n }\n\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n <div className=\"shrink-0 border-b border-border\">\n <Toolbar>\n <ToolbarGroup>\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label={translate('builder.canvas.undo')}\n title={translate('builder.canvas.undo')}\n disabled={readOnly || !builder.canUndo}\n onClick={() => dispatch({ type: 'undo' })}\n >\n <Undo2 />\n </Button>\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label={translate('builder.canvas.redo')}\n title={translate('builder.canvas.redo')}\n disabled={readOnly || !builder.canRedo}\n onClick={() => dispatch({ type: 'redo' })}\n >\n <Redo2 />\n </Button>\n </ToolbarGroup>\n <ToolbarSeparator />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n disabled={readOnly}\n onClick={() => openInsert(builder.selectedId ?? rootId)}\n >\n <Plus /> {translate('builder.canvas.insert')}\n </Button>\n {/* Breakpoint preview-width switcher (EC-211/EC-250), centered over the\n canvas via flanking spacers. A preview control, not an edit — stays\n active even in read-only published preview. */}\n <Spacer />\n <BreakpointSwitcher />\n <Spacer />\n </Toolbar>\n </div>\n\n <div className=\"grid min-h-0 flex-1 grid-cols-[minmax(200px,240px)_1fr_minmax(280px,320px)] gap-px bg-border\">\n <Panel\n className={cn('rounded-none border-0', readOnly && 'pointer-events-none opacity-60')}\n >\n <PageTree onAddChild={openInsert} />\n </Panel>\n <div\n className={cn(\n 'flex min-h-0 flex-col bg-background',\n readOnly && 'pointer-events-none opacity-60',\n )}\n >\n {/* EC-300/AD-16: the read-only Published view resolves references against\n the published perspective (honest preview); the editable Draft view\n resolves drafts (a just-made reusable shows immediately). */}\n <BuilderCanvas perspective={readOnly ? 'published' : 'draft'} />\n </div>\n <InspectorSidebar panes={panes} />\n </div>\n\n <InsertMenu context={insertContext} open={insertOpen} onOpenChange={setInsertOpen} />\n </div>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"composition-field-editor.js","sourceRoot":"","sources":["../../../src/components/builder/composition-field-editor.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useMemo, useRef, useState } from 'react'\nimport { AlertTriangle, Plus, Redo2, SlidersHorizontal, Undo2 } from 'lucide-react'\nimport { DEFAULT_SLOT, blockRootsOf } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport type { BlocksFieldDef } from '@elytracms/core/cms-core'\nimport { Button, Panel, Row, Spacer, Text, Toolbar, ToolbarGroup, ToolbarSeparator } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { BuilderProvider, useBuilder } from './builder-store'\nimport { useCanvasViewports } from './canvas-breakpoints'\nimport type { ViewportOption } from './canvas-breakpoints'\nimport { BuilderCanvas } from './canvas'\nimport { PageTree } from './page-tree'\nimport { PropertyInspector } from './property-inspector'\nimport { InspectorSidebar } from '../inspector/inspector-sidebar'\nimport type { InspectorPane } from '../inspector/inspector-sidebar'\nimport { InsertMenu } from './insert-menu'\nimport type { InsertContext } from './insert-menu'\nimport { resolveInsertContext } from './insert-context'\nimport { ReuseProvider } from './reuse-context'\n\n/**\n * EC-188 — the block-field surface: one canvas engine mounted on a composition\n * **field value** (AD-12), not the project graph.\n *\n * The composition tree is wrapped in a synthetic single-page graph and edited by\n * a SCOPED `BuilderProvider` with **no `projectId`** (so the graph persister is a\n * no-op — builder-store.tsx). Every committed edit is lifted out via `onChange`\n * into the CMS document draft; the document's own Save persists it (the tree is\n * stored as document data, `v.any()`). This reuses the real canvas + inspector +\n * blocks panel unchanged — the same EC-158–164 mechanics — without touching the\n * project-wide builder store. Surfaced on a dedicated \"Canvas\" fieldset tab in\n * the CMS detail editor so the page builder gets full screen real estate rather\n * than being crammed into the field list.\n */\n\nconst SYNTH_ROOT_ID = 'composition.root'\n/** The implicit root container the field's blocks compose inside (like a page root). */\nconst SYNTH_ROOT_COMPONENT = 'base.Stack'\n\n/**\n * Wrap the field value's blocks into a synthetic root container (EC-287). The\n * builder store edits this single root-bearing composition directly; its\n * children are the field's blocks, lifted back out via `onChange`.\n */\nfunction buildSyntheticRoot(value: unknown): ComponentNode {\n return {\n id: SYNTH_ROOT_ID,\n componentId: SYNTH_ROOT_COMPONENT,\n slots: { [DEFAULT_SLOT]: blockRootsOf(value) },\n }\n}\n\n/** Unwrap the synthetic root's children back to a field value per cardinality. */\nfunction unwrapValue(blocks: readonly ComponentNode[], multiple: boolean): unknown {\n return multiple ? [...blocks] : blocks[0]\n}\n\nexport function CompositionFieldEditor({\n field,\n value,\n onChange,\n readOnly,\n trailingPanes,\n}: {\n field: BlocksFieldDef\n value: unknown\n onChange: (value: unknown) => void\n readOnly: boolean\n /** Inspector panes the document editor appends after the canvas-supplied\n * `Properties` pane (EC-224 — the frame-owned `Versions` pane). */\n trailingPanes?: readonly InspectorPane[]\n}) {\n // Build the synthetic root ONCE from the value at mount. The editor is\n // uncontrolled thereafter — it is the source of truth while open and lifts\n // changes out via onChange; re-opening the tab remounts from the current draft.\n // (BuilderProvider reads `composition` only in its lazy initial state, so later\n // prop changes are ignored regardless.)\n const initialRoot = useMemo(() => buildSyntheticRoot(value), [])\n\n return (\n <BuilderProvider composition={initialRoot}>\n <CompositionLift multiple={field.multiple} onChange={onChange} readOnly={readOnly} />\n <ReuseProvider>\n <CompositionEditorShell\n readOnly={readOnly}\n fieldAllow={field.allow}\n trailingPanes={trailingPanes}\n />\n </ReuseProvider>\n </BuilderProvider>\n )\n}\n\n/**\n * Lifts committed edits out of the scoped store into the CMS draft. The first\n * render establishes the (container-normalized) baseline so simply opening the\n * tab never marks the document dirty; every subsequent change to the block list\n * emits the unwrapped value.\n */\nfunction CompositionLift({\n multiple,\n onChange,\n readOnly,\n}: {\n multiple: boolean\n onChange: (value: unknown) => void\n readOnly: boolean\n}) {\n const builder = useBuilder()\n const blocks = builder.present.slots?.[DEFAULT_SLOT] ?? EMPTY_BLOCKS\n const onChangeRef = useRef(onChange)\n onChangeRef.current = onChange\n const baselineRef = useRef<readonly ComponentNode[] | null>(null)\n\n useEffect(() => {\n // EC-226: never lift edits out of a READ-ONLY canvas (the Published view).\n // The canvas's keyboard listeners (clipboard paste/cut) aren't blocked by the\n // visual `pointer-events-none`, so without this a Cmd+V could mutate the\n // graph; refusing to lift keeps the document draft intact regardless. The\n // editor remounts per perspective, so any transient in-canvas change is\n // discarded on switch.\n if (readOnly) return\n if (baselineRef.current === null) {\n baselineRef.current = blocks\n return\n }\n if (blocks !== baselineRef.current) {\n baselineRef.current = blocks\n onChangeRef.current(unwrapValue(blocks, multiple))\n }\n }, [blocks, multiple, readOnly])\n\n return null\n}\n\nconst EMPTY_BLOCKS: readonly ComponentNode[] = []\n\n/** A breakpoint preview-width toggle (EC-211/EC-250). Switching the viewport\n * resizes only the preview frame (not an undoable edit, writes no graph command).\n * The label is the breakpoint name (`Full`/`sm`/`md`/…) with the real px width in\n * the tooltip — so the switcher reflects the site's actual screens, not a fixed\n * desktop/tablet/phone triple. */\nfunction ViewportButton({ option }: { option: ViewportOption }) {\n const builder = useBuilder()\n const active = builder.viewport === option.key\n return (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={translate('builder.canvas.viewportPreviewAria', { label: option.label })}\n aria-pressed={active}\n title={\n option.px !== undefined\n ? `${option.label} · ${option.px}px`\n : translate('builder.canvas.viewportWidthTitle', { label: option.label })\n }\n onClick={() => builder.dispatch({ type: 'viewport', viewport: option.key })}\n className={cn('px-2 text-xs', active && 'bg-accent text-accent-foreground')}\n >\n {option.label}\n </Button>\n )\n}\n\n/** The centered breakpoint switcher (EC-250): the resolved viewport set, plus a\n * visible validation issue when a project declared breakpoints but none are usable. */\nfunction BreakpointSwitcher() {\n const { viewports, issue } = useCanvasViewports()\n return (\n <Row gap=\"1\" align=\"center\">\n <ToolbarGroup>\n {viewports.map((v) => (\n <ViewportButton key={v.key} option={v} />\n ))}\n </ToolbarGroup>\n {issue ? (\n <Row gap=\"1\" align=\"center\" className=\"text-amber-600 dark:text-amber-400\" title={issue}>\n <AlertTriangle aria-hidden className=\"size-3.5\" />\n <Text size=\"xs\">{translate('builder.canvas.breakpointsIssueLabel')}</Text>\n </Row>\n ) : null}\n </Row>\n )\n}\n\n/**\n * The slim editor surface — the canvas + layers + inspector, plus Insert and\n * undo/redo. It deliberately drops the project-builder *lifecycle* chrome\n * (publish, versions, AI, page SEO): the document owns the lifecycle, not the\n * field. It keeps spatial/preview controls — including the breakpoint\n * preview-width switcher (EC-211), a canvas concern, not a lifecycle one.\n */\nfunction CompositionEditorShell({\n readOnly,\n fieldAllow,\n trailingPanes,\n}: {\n readOnly: boolean\n /** The field's top-level component vocabulary (EC-188) — the leash applied to\n * inserts at the composition root so forbidden components aren't even offered. */\n fieldAllow?: readonly string[]\n /** Document-level panes appended after `Properties` (EC-224). */\n trailingPanes?: readonly InspectorPane[]\n}) {\n const builder = useBuilder()\n const dispatch = builder.dispatch\n const [insertContext, setInsertContext] = useState<InsertContext | null>(null)\n const [insertOpen, setInsertOpen] = useState(false)\n const rootId = builder.present.id\n\n // The canvas SUPPLIES the `Properties` pane (EC-224); the document editor\n // appends its own (`Versions`). Properties dims under read-only just as the\n // whole inspector column used to — but the appended panes stay interactive.\n const propertiesPane = useMemo<InspectorPane>(\n () => ({\n id: 'properties',\n label: translate('builder.inspector.propertiesPane'),\n icon: <SlidersHorizontal className=\"size-3.5\" />,\n render: () => (\n <div\n className={cn(\n 'h-full min-h-0 overflow-hidden',\n readOnly && 'pointer-events-none opacity-60',\n )}\n >\n <PropertyInspector />\n </div>\n ),\n }),\n [readOnly],\n )\n const panes = useMemo<InspectorPane[]>(\n () => [propertiesPane, ...(trailingPanes ?? [])],\n [propertiesPane, trailingPanes],\n )\n\n function openInsert(nodeId: string) {\n const ctx = resolveInsertContext(builder, nodeId, fieldAllow)\n if (ctx) {\n setInsertContext(ctx)\n setInsertOpen(true)\n }\n }\n\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n <div className=\"shrink-0 border-b border-border\">\n <Toolbar>\n <ToolbarGroup>\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label={translate('builder.canvas.undo')}\n title={translate('builder.canvas.undo')}\n disabled={readOnly || !builder.canUndo}\n onClick={() => dispatch({ type: 'undo' })}\n >\n <Undo2 />\n </Button>\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label={translate('builder.canvas.redo')}\n title={translate('builder.canvas.redo')}\n disabled={readOnly || !builder.canRedo}\n onClick={() => dispatch({ type: 'redo' })}\n >\n <Redo2 />\n </Button>\n </ToolbarGroup>\n <ToolbarSeparator />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n disabled={readOnly}\n onClick={() => openInsert(builder.selectedId ?? rootId)}\n >\n <Plus /> {translate('builder.canvas.insert')}\n </Button>\n {/* Breakpoint preview-width switcher (EC-211/EC-250), centered over the\n canvas via flanking spacers. A preview control, not an edit — stays\n active even in read-only published preview. */}\n <Spacer />\n <BreakpointSwitcher />\n <Spacer />\n </Toolbar>\n </div>\n\n <div className=\"grid min-h-0 flex-1 grid-cols-[minmax(200px,240px)_1fr_minmax(280px,320px)] gap-px bg-border\">\n <Panel\n className={cn('rounded-none border-0', readOnly && 'pointer-events-none opacity-60')}\n >\n <PageTree onAddChild={openInsert} />\n </Panel>\n <div\n className={cn(\n 'flex min-h-0 flex-col bg-background',\n readOnly && 'pointer-events-none opacity-60',\n )}\n >\n {/* EC-300/AD-16: the read-only Published view resolves references against\n the published perspective (honest preview); the editable Draft view\n resolves drafts (a just-made reusable shows immediately). */}\n <BuilderCanvas perspective={readOnly ? 'published' : 'draft'} />\n </div>\n <InspectorSidebar panes={panes} />\n </div>\n\n <InsertMenu context={insertContext} open={insertOpen} onOpenChange={setInsertOpen} />\n </div>\n )\n}\n"]}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { InsertContext } from './insert-menu';
|
|
2
2
|
import { useBuilder } from './builder-store';
|
|
3
|
+
/**
|
|
4
|
+
* Where an insert lands: the canvas insert-context resolution, shared by every
|
|
5
|
+
* surface that mounts the canvas engine.
|
|
6
|
+
*
|
|
7
|
+
* Extracted from the retired standalone `BuilderShell` (EC-210) so the in-CMS
|
|
8
|
+
* Canvas editor (`composition-field-editor.tsx`, EC-188/AD-12) can own it without
|
|
9
|
+
* importing the dead shell. No UI here — pure graph/registry resolution.
|
|
10
|
+
*/
|
|
3
11
|
/**
|
|
4
12
|
* Resolve where an insert lands: into the node's child slot, else as a sibling.
|
|
5
13
|
*
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getNode, getParentInfo } from '@elytracms/core/project-graph';
|
|
2
2
|
import { getStudioRegistry } from '@elytracms/studio/lib/registry';
|
|
3
|
+
import { shortId } from '@elytracms/studio/lib/persona/labels';
|
|
3
4
|
import { intersectAllow } from './blocks-model';
|
|
4
5
|
import { useBuilder } from './builder-store';
|
|
5
6
|
/**
|
|
@@ -10,9 +11,6 @@ import { useBuilder } from './builder-store';
|
|
|
10
11
|
* Canvas editor (`composition-field-editor.tsx`, EC-188/AD-12) can own it without
|
|
11
12
|
* importing the dead shell. No UI here — pure graph/registry resolution.
|
|
12
13
|
*/
|
|
13
|
-
function shortId(componentId) {
|
|
14
|
-
return componentId.replace('base.', '').replace('project.', '');
|
|
15
|
-
}
|
|
16
14
|
/**
|
|
17
15
|
* Resolve where an insert lands: into the node's child slot, else as a sibling.
|
|
18
16
|
*
|