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:
@@ -1626,6 +1626,65 @@ body {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------
|
||||
Mobile selection toolbar (MobileSelectionToolbar.tsx, Phase B item 2)
|
||||
Bottom-fixed just above the tab bar -- simplest, robust positioning
|
||||
that never has to track a scrolling/resizing node's DOM rect. Sits at
|
||||
--z-selection-toolbar (600), above the tab bar (500) and sheet backdrop
|
||||
(400) so it's never occluded by either, but well below --z-modal
|
||||
(5000). Hidden entirely (component returns null) whenever a sheet is
|
||||
open, so it never has to fight the sheet for the same screen space.
|
||||
-------------------------------------------------------------------- */
|
||||
.mobile-selection-toolbar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: calc(var(--mobile-tabbar-height) + env(safe-area-inset-bottom, 0px));
|
||||
z-index: var(--z-selection-toolbar);
|
||||
display: flex;
|
||||
background: var(--color-bg-elevated);
|
||||
border-top: 1px solid var(--color-border);
|
||||
box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.35);
|
||||
padding: 4px calc(2px + env(safe-area-inset-right, 0px)) 4px calc(2px + env(safe-area-inset-left, 0px));
|
||||
}
|
||||
|
||||
.mobile-selection-toolbar-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
min-height: 44px;
|
||||
padding: 4px 2px;
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: var(--color-text);
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.mobile-selection-toolbar-btn i {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.mobile-selection-toolbar-btn:disabled {
|
||||
color: var(--color-text-dim);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.mobile-selection-toolbar-btn.danger {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.mobile-selection-toolbar-btn.danger.confirming {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------
|
||||
Bottom sheet (BottomSheet.tsx)
|
||||
-------------------------------------------------------------------- */
|
||||
@@ -1637,8 +1696,13 @@ body {
|
||||
/* Stop above the bottom tab bar (rather than a full-viewport `inset: 0`)
|
||||
so the tab bar is never covered/blocked by the backdrop -- tapping a
|
||||
different tab while a sheet is open swaps sheets directly instead of
|
||||
being swallowed by the overlay sitting on top of it. */
|
||||
bottom: calc(var(--mobile-tabbar-height) + env(safe-area-inset-bottom, 0px));
|
||||
being swallowed by the overlay sitting on top of it.
|
||||
`--keyboard-inset` (item 5, Phase B) is set inline by BottomSheet.tsx
|
||||
from `useVisualViewportInsets` -- 0px when no on-screen keyboard is
|
||||
open (the default below is the no-JS/unsupported-browser fallback),
|
||||
otherwise the keyboard's height, so the whole sheet lifts clear of it
|
||||
instead of the keyboard covering its lower portion. */
|
||||
bottom: calc(var(--mobile-tabbar-height) + env(safe-area-inset-bottom, 0px) + var(--keyboard-inset, 0px));
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: var(--z-sheet-backdrop);
|
||||
display: flex;
|
||||
@@ -1647,8 +1711,12 @@ body {
|
||||
|
||||
.mobile-sheet {
|
||||
width: 100%;
|
||||
max-height: 65vh;
|
||||
max-height: 65dvh;
|
||||
/* Capped a second time by the keyboard inset so the sheet's own
|
||||
max-height shrinks along with the lifted backdrop -- without this the
|
||||
sheet could still try to be 65dvh tall from its new (higher) bottom
|
||||
edge and overflow off the top of the screen. */
|
||||
max-height: calc(65vh - var(--keyboard-inset, 0px));
|
||||
max-height: calc(65dvh - var(--keyboard-inset, 0px));
|
||||
background: var(--color-bg-surface);
|
||||
border-top-left-radius: 16px;
|
||||
border-top-right-radius: 16px;
|
||||
@@ -1664,6 +1732,10 @@ body {
|
||||
justify-content: center;
|
||||
padding: 8px 0 4px;
|
||||
flex-shrink: 0;
|
||||
/* Swipe-to-dismiss (item 4, Phase B) tracks its own touchmove here --
|
||||
`none` stops the browser's native scroll/rubber-band gesture from
|
||||
competing with it. */
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.mobile-sheet-handle {
|
||||
@@ -1680,6 +1752,8 @@ body {
|
||||
padding: 4px 12px 10px 16px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
flex-shrink: 0;
|
||||
/* Swipe-to-dismiss (item 4, Phase B) -- see .mobile-sheet-handle-row. */
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.mobile-sheet-title {
|
||||
@@ -1749,6 +1823,15 @@ body {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
/* Layers panel rows (item 4) -- the reliable fallback selection path when
|
||||
a node is hard to hit precisely on the canvas by touch. Bumped to the
|
||||
44px touch-target minimum; tapping a row already selects (LayerNode's
|
||||
`handleActivate`), unchanged. */
|
||||
.mobile-sheet-body .layer-node-row {
|
||||
min-height: 44px;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------
|
||||
Templates modal: category tabs stay horizontally scrollable and never
|
||||
shrink below a comfortable touch target; close button enlarged to the
|
||||
|
||||
Reference in New Issue
Block a user