Add Craft.js site builder (v2) - complete rebuild from GrapesJS

Rebuilt the visual site builder from scratch using Craft.js, React 18,
and TypeScript. The new editor renders directly in the DOM (no iframe),
supports 40+ components, multi-page with shared header/footer, 16
templates, full-spectrum color/gradient controls, custom head code
injection, save/publish workflow, and auto-save.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-05 18:31:16 -07:00
parent b511a6684d
commit 91a6b6f34b
103 changed files with 26296 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
import { useEffect } from 'react';
import { useEditor } from '@craftjs/core';
function isInputFocused(): boolean {
const el = document.activeElement;
if (!el) return false;
const tag = el.tagName.toLowerCase();
if (tag === 'input' || tag === 'textarea' || tag === 'select') return true;
if ((el as HTMLElement).isContentEditable) return true;
return false;
}
export function useKeyboardShortcuts() {
const { actions, query } = useEditor((state) => {
const selectedIds = state.events.selected;
const selectedId = selectedIds ? Array.from(selectedIds)[0] : null;
return { selectedId };
});
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Skip when typing in inputs
if (isInputFocused()) return;
const ctrl = e.ctrlKey || e.metaKey;
// Ctrl+Z: Undo
if (ctrl && !e.shiftKey && e.key === 'z') {
e.preventDefault();
try {
actions.history.undo();
} catch (err) {
// No more undo steps
}
return;
}
// Ctrl+Y or Ctrl+Shift+Z: Redo
if ((ctrl && e.key === 'y') || (ctrl && e.shiftKey && e.key === 'z') || (ctrl && e.shiftKey && e.key === 'Z')) {
e.preventDefault();
try {
actions.history.redo();
} catch (err) {
// No more redo steps
}
return;
}
// Delete/Backspace: delete selected node
if (e.key === 'Delete' || e.key === 'Backspace') {
e.preventDefault();
try {
const selected = query.getEvent('selected').all();
if (selected.length > 0) {
const nodeId = selected[0];
if (nodeId !== 'ROOT') {
actions.delete(nodeId);
}
}
} catch (err) {
console.error('Delete failed:', err);
}
return;
}
// Ctrl+D: duplicate selected
if (ctrl && (e.key === 'd' || e.key === 'D')) {
e.preventDefault();
try {
const selected = query.getEvent('selected').all();
if (selected.length > 0) {
const nodeId = selected[0];
if (nodeId !== 'ROOT') {
const node = query.node(nodeId).get();
const parentId = node?.data?.parent;
if (parentId) {
const tree = query.node(nodeId).toNodeTree();
actions.addNodeTree(tree, parentId);
}
}
}
} catch (err) {
console.error('Duplicate failed:', err);
}
return;
}
// Escape: deselect all
if (e.key === 'Escape') {
e.preventDefault();
try {
actions.clearEvents();
} catch (err) {
// Ignore
}
return;
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [actions, query]);
}