Files
site-builder/craft/src/editor/EditorShell.tsx
T
shadowdao 2a8a26687b fix(builder): mobile-A2 hardening -- 16px inputs, shared sheet/modal chrome, z-scale + portal
- Force font-size:16px !important on Styles-sheet/topbar/Sitesmith inputs
  inside the mobile media query so inline 12px/14px styles stop triggering
  iOS zoom-on-focus.
- Lift sheet-open + Templates/Head Code modal-open state out of private
  useState into a shared MobileChromeContext (EditorShell), so Phase B can
  open/close sheets from outside MobilePanelBar.
- Add an explicit z-index layer scale, portal TemplateModal to
  document.body (was trapped under the tab bar inside .topbar's stacking
  context), align Sitesmith to the same --z-modal layer, and make opening
  a sheet close any open modal. Also fix modal backdrops swallowing tab
  bar taps (mirrors the sheet backdrop's existing tab-bar cutout).
- Drop BottomSheet's incorrect aria-modal; mobile-aware AssetsPanel empty
  state copy.
- Tests: useIsMobile (matchMedia mock incl. legacy fallback + cleanup),
  MobileChromeContext invariants (one sheet open, sheet closes modals),
  MobilePanelBar wiring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 07:30:12 -07:00

111 lines
4.3 KiB
TypeScript

import React, { useState, useCallback, useEffect, useRef } from 'react';
import { useEditor } from '@craftjs/core';
import { TopBar } from '../panels/topbar/TopBar';
import { LeftPanel } from '../panels/left/LeftPanel';
import { RightPanel } from '../panels/right/RightPanel';
import { MobilePanelBar } from '../panels/mobile/MobilePanelBar';
import { Canvas } from './Canvas';
import { ContextMenu } from '../panels/context-menu/ContextMenu';
import { useContextMenu } from '../hooks/useContextMenu';
import { useKeyboardShortcuts } from '../hooks/useKeyboardShortcuts';
import { useIsMobile } from '../hooks/useIsMobile';
import { MobileChromeProvider } from '../state/MobileChromeContext';
import { DeviceMode } from '../types';
const SHOW_GUIDES_STORAGE_KEY = 'craft-show-guides';
function loadShowGuides(): boolean {
try {
const stored = window.localStorage.getItem(SHOW_GUIDES_STORAGE_KEY);
return stored === null ? true : stored === '1';
} catch {
return true;
}
}
export const EditorShell: React.FC = () => {
const isMobile = useIsMobile();
const [device, setDevice] = useState<DeviceMode>('desktop');
// Phase A: default the canvas to the "mobile" preview width the first
// time we detect a mobile viewport, so the frame fits without the user
// having to reach for the device switcher (now tucked in TopBar's
// overflow menu on mobile). Only fires once, and only if the device is
// still at its initial default -- it must not fight a device the user
// has already picked (e.g. from the overflow menu) on a later re-render.
const mobileDeviceAppliedRef = useRef(false);
useEffect(() => {
if (isMobile && !mobileDeviceAppliedRef.current) {
mobileDeviceAppliedRef.current = true;
setDevice((current) => (current === 'desktop' ? 'mobile' : current));
}
}, [isMobile]);
// Item 10: canvas dashed "guide" outlines toggle -- default ON, persisted
// so the choice survives a reload. Lifted here (rather than owned by
// TopBar or Canvas alone) because the toggle button lives in TopBar but
// the `.guides-off` class it drives is applied to Canvas's
// `.canvas-device-frame`, mirroring how `device` is already lifted for
// the same reason.
const [showGuides, setShowGuidesState] = useState<boolean>(loadShowGuides);
const setShowGuides = useCallback((next: boolean) => {
setShowGuidesState(next);
try {
window.localStorage.setItem(SHOW_GUIDES_STORAGE_KEY, next ? '1' : '0');
} catch {
// Storage unavailable (private browsing, etc.) -- in-memory state still works.
}
}, []);
const { menuState, show: showMenu, hide: hideMenu } = useContextMenu();
const { query } = useEditor();
// Register keyboard shortcuts
useKeyboardShortcuts();
const handleContextMenu = useCallback((e: React.MouseEvent) => {
e.preventDefault();
// Find the selected node id
let nodeId: string | null = null;
try {
const selected = query.getEvent('selected').all();
if (selected.length > 0) {
nodeId = selected[0];
}
} catch {
// No selection
}
showMenu(e.clientX, e.clientY, nodeId);
}, [query, showMenu]);
return (
// Mobile-A2: shared sheet/modal-open state (see MobileChromeContext) --
// provided around the whole shell so TopBar's Templates/Head Code modal
// state and MobilePanelBar's sheet state live in one place. Desktop
// doesn't render MobilePanelBar and TopBar's desktop branch behaves
// identically to before (same booleans, just sourced from context).
<MobileChromeProvider>
<div className="editor-app">
<TopBar
device={device}
onDeviceChange={setDevice}
showGuides={showGuides}
onToggleGuides={() => setShowGuides(!showGuides)}
/>
<div className="editor-container">
{!isMobile && <LeftPanel />}
<div onContextMenu={handleContextMenu} style={{ flex: 1, display: 'flex', minWidth: 0 }}>
<Canvas device={device} showGuides={showGuides} />
</div>
{!isMobile && <RightPanel />}
</div>
{isMobile && <MobilePanelBar />}
<ContextMenu
visible={menuState.visible}
x={menuState.x}
y={menuState.y}
nodeId={menuState.nodeId}
onClose={hideMenu}
/>
</div>
</MobileChromeProvider>
);
};