diff --git a/craft/src/editor/Canvas.tsx b/craft/src/editor/Canvas.tsx index a4a2ca5..25a46af 100644 --- a/craft/src/editor/Canvas.tsx +++ b/craft/src/editor/Canvas.tsx @@ -1,5 +1,5 @@ import React, { useMemo, useRef, useEffect } from 'react'; -import { Frame, Element } from '@craftjs/core'; +import { Frame, Element, useEditor } from '@craftjs/core'; import { Container } from '../components/layout/Container'; import { usePages } from '../state/PageContext'; import { DeviceMode } from '../types'; @@ -93,6 +93,33 @@ const ZonePreview: React.FC<{ craftState: string | null; zone: 'header' | 'foote ); }; +/** + * First-run hint shown over the canvas drop area once the current page's + * root node exists and has no children yet. Hidden the instant something + * is dropped in, and while a drag is in progress (so it never fights the + * drop-target UI). `pointer-events: none` (see .empty-canvas-hint in + * editor.css) keeps it from intercepting clicks/drops meant for the + * underlying empty canvas. + */ +export const EmptyCanvasHint: React.FC = () => { + const { isEmpty, isDragging } = useEditor((state) => { + const root = state.nodes['ROOT']; + return { + isEmpty: !!root && root.data.nodes.length === 0, + isDragging: state.events.dragged.size > 0, + }; + }); + + if (!isEmpty || isDragging) return null; + + return ( +
+ + Drag blocks from the left panel, or pick a Template to start. +
+ ); +}; + export const Canvas: React.FC = ({ device }) => { const width = DEVICE_WIDTHS[device]; const { isEditingHeader, isEditingFooter, headerPage, footerPage } = usePages(); @@ -140,14 +167,17 @@ export const Canvas: React.FC = ({ device }) => { )} - - - +
+ + + + {isEditingRegularPage && } +
{isEditingRegularPage && ( diff --git a/craft/src/editor/EmptyCanvasHint.test.tsx b/craft/src/editor/EmptyCanvasHint.test.tsx new file mode 100644 index 0000000..e1c28b8 --- /dev/null +++ b/craft/src/editor/EmptyCanvasHint.test.tsx @@ -0,0 +1,67 @@ +import { describe, test, expect, vi, beforeEach } from 'vitest'; +import React from 'react'; +import { createRoot, Root } from 'react-dom/client'; +import { act } from 'react-dom/test-utils'; + +/* Same DOM-harness pattern as Footer.editguard.test.tsx: mock @craftjs/core's + useEditor so we can drive editor state without a real tree. */ +let mockNodes: Record = {}; +let mockDraggedSize = 0; + +vi.mock('@craftjs/core', () => ({ + useEditor: (collect: (state: any) => any) => + collect({ + nodes: mockNodes, + events: { dragged: { size: mockDraggedSize } }, + }), +})); + +import { EmptyCanvasHint } from './Canvas'; + +let container: HTMLDivElement; +let root: Root; + +function render(ui: React.ReactElement) { + container = document.createElement('div'); + document.body.appendChild(container); + act(() => { + root = createRoot(container); + root.render(ui); + }); +} + +beforeEach(() => { + mockNodes = {}; + mockDraggedSize = 0; +}); + +describe('EmptyCanvasHint', () => { + test('renders nothing before ROOT has mounted (no root node yet)', () => { + render(); + expect(container.querySelector('.empty-canvas-hint')).toBeNull(); + container.remove(); + }); + + test('renders the hint once ROOT exists with zero children', () => { + mockNodes = { ROOT: { data: { nodes: [] } } }; + render(); + expect(container.querySelector('.empty-canvas-hint')).not.toBeNull(); + expect(container.textContent).toContain('Drag blocks from the left panel'); + container.remove(); + }); + + test('hides once the page has content', () => { + mockNodes = { ROOT: { data: { nodes: ['node-1'] } } }; + render(); + expect(container.querySelector('.empty-canvas-hint')).toBeNull(); + container.remove(); + }); + + test('hides while a drag is in progress, even on an empty root', () => { + mockNodes = { ROOT: { data: { nodes: [] } } }; + mockDraggedSize = 1; + render(); + expect(container.querySelector('.empty-canvas-hint')).toBeNull(); + container.remove(); + }); +}); diff --git a/craft/src/styles/editor.css b/craft/src/styles/editor.css index 2937a34..e2754b2 100644 --- a/craft/src/styles/editor.css +++ b/craft/src/styles/editor.css @@ -1225,6 +1225,8 @@ body { Empty Canvas State -------------------------------------------------------------------------- */ .empty-canvas-hint { + position: absolute; + inset: 0; display: flex; flex-direction: column; align-items: center; @@ -1234,6 +1236,9 @@ body { font-size: 13px; text-align: center; gap: 12px; + /* Overlays the drop area without intercepting drags/clicks meant for the + underlying (empty) canvas root -- see Canvas.tsx. */ + pointer-events: none; } .empty-canvas-hint i {