feat(builder): mobile-B touch editing -- selection toolbar, tap-to-add, swipe-dismiss
Phase B makes the Craft.js editor genuinely usable by touch on top of Phase A's responsive shell, gated entirely behind useIsMobile()/<=768px: - Extract useNodeActions(nodeId) out of ContextMenu.tsx (move/duplicate/ delete/select-parent), shared by the desktop right-click menu (behavior unchanged) and the new mobile MobileSelectionToolbar. - MobileSelectionToolbar: bottom-fixed selection toolbar (Move Up/Down, Duplicate, Select Parent, Edit Styles, two-tap Delete confirm), hidden while a sheet is open. - BlocksPanel: tap-to-add on mobile (insert after selection, close sheet, select + scroll the new node into view); desktop drag/double-click unchanged. - LayersPanel rows >=44px on mobile; HeadCodeModal portaled to document.body (same fix TemplateModal already had); BottomSheet gets swipe-to-dismiss and on-screen-keyboard clearance via a new useVisualViewportInsets hook. Also fixes two pre-existing bugs surfaced only by driving a real Craft.js document with Playwright touch input (masked by tests that mock @craftjs/core): regenerateTreeIds structuredClone'd a live node's whole data object, including the component function reference in data.type, throwing DataCloneError and silently breaking Duplicate/Paste for every node type; and an earlier useNodeActions draft cached canMoveUp/canMoveDown inside a useEditor collector closed over nodeId, which goes stale for one render whenever the selection changes without an unrelated store event. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -366,12 +366,27 @@ export function regenerateTreeIds(tree: NodeTree): NodeTree {
|
||||
newLinkedNodes[slot] = remapId(linkedId);
|
||||
}
|
||||
|
||||
// Deep-clone data so mutable sub-objects (props, custom, etc.) are never
|
||||
// shared by reference between the original node and its regenerated
|
||||
// copy. Craft.js's setProp mutates data.props in place, so a shallow
|
||||
// copy here would let edits to the duplicate silently corrupt the
|
||||
// original.
|
||||
const clonedData = structuredClone(oldNode.data);
|
||||
// Deep-clone only the mutable sub-objects (props, custom) so they're
|
||||
// never shared by reference between the original node and its
|
||||
// regenerated copy -- Craft.js's setProp mutates data.props in place, so
|
||||
// a shallow copy here would let edits to the duplicate silently corrupt
|
||||
// the original.
|
||||
//
|
||||
// Deliberately NOT `structuredClone(oldNode.data)` as a whole: for a
|
||||
// LIVE Craft.js node (as opposed to a plain serialized one), `data.type`
|
||||
// is the actual component function/class reference (see the long
|
||||
// comment on `buildNodeTree` above) -- `structuredClone` cannot clone a
|
||||
// function and throws `DataCloneError`, which silently broke EVERY
|
||||
// duplicate/paste in the real app (masked in unit tests that mock
|
||||
// `@craftjs/core` with plain-data fake nodes, so `toNodeTree()` never
|
||||
// actually returns a function there). `type` is a stable reference that
|
||||
// both the original and the duplicate should point at unchanged, so it
|
||||
// never needed cloning in the first place.
|
||||
const clonedData = {
|
||||
...oldNode.data,
|
||||
props: structuredClone(oldNode.data.props),
|
||||
custom: structuredClone(oldNode.data.custom),
|
||||
};
|
||||
|
||||
const newNode: Node = {
|
||||
...oldNode,
|
||||
|
||||
Reference in New Issue
Block a user