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('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(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).
setShowGuides(!showGuides)} />
{!isMobile && }
{!isMobile && }
{isMobile && }
); };