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 {