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:
@@ -0,0 +1,137 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useEditor } from '@craftjs/core';
|
||||
|
||||
interface LayerNodeProps {
|
||||
nodeId: string;
|
||||
depth: number;
|
||||
}
|
||||
|
||||
const LayerNode: React.FC<LayerNodeProps> = ({ nodeId, depth }) => {
|
||||
const { node, selectedId, actions } = useEditor((state) => {
|
||||
const n = state.nodes[nodeId];
|
||||
const selectedIds = state.events.selected;
|
||||
const selId = selectedIds ? Array.from(selectedIds)[0] : null;
|
||||
return {
|
||||
node: n,
|
||||
selectedId: selId,
|
||||
};
|
||||
});
|
||||
|
||||
const handleClick = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
actions.selectNode(nodeId);
|
||||
}, [actions, nodeId]);
|
||||
|
||||
if (!node) return null;
|
||||
|
||||
const isSelected = selectedId === nodeId;
|
||||
const nodeType = node.data.type;
|
||||
const resolvedName = typeof nodeType === 'object' && nodeType !== null && 'resolvedName' in nodeType
|
||||
? (nodeType as any).resolvedName
|
||||
: typeof nodeType === 'string' ? nodeType : undefined;
|
||||
const displayName = node.data.displayName || resolvedName || 'Component';
|
||||
const childNodeIds: string[] = node.data.nodes || [];
|
||||
const linkedNodeIds: string[] = Object.values(node.data.linkedNodes || {}) as string[];
|
||||
const allChildren = [...childNodeIds, ...linkedNodeIds];
|
||||
const isRoot = nodeId === 'ROOT';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
onClick={handleClick}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '5px 8px',
|
||||
paddingLeft: `${8 + depth * 16}px`,
|
||||
fontSize: 12,
|
||||
fontWeight: isSelected ? 600 : 400,
|
||||
color: isSelected ? 'var(--color-accent)' : 'var(--color-text)',
|
||||
background: isSelected ? 'var(--color-accent-subtle)' : 'transparent',
|
||||
borderLeft: isSelected ? '2px solid var(--color-accent)' : '2px solid transparent',
|
||||
cursor: 'pointer',
|
||||
transition: 'all var(--transition-fast)',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
userSelect: 'none',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isSelected) {
|
||||
(e.currentTarget as HTMLElement).style.background = 'var(--color-bg-hover)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isSelected) {
|
||||
(e.currentTarget as HTMLElement).style.background = 'transparent';
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Indentation indicator */}
|
||||
{allChildren.length > 0 && (
|
||||
<span style={{ marginRight: 4, fontSize: 8, color: 'var(--color-text-dim)' }}>
|
||||
▼
|
||||
</span>
|
||||
)}
|
||||
{allChildren.length === 0 && (
|
||||
<span style={{ marginRight: 4, fontSize: 8, color: 'transparent' }}>
|
||||
▼
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Component type icon and name */}
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{isRoot ? 'Canvas (Root)' : displayName}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Render children */}
|
||||
{allChildren.map((childId) => (
|
||||
<LayerNode key={childId} nodeId={childId} depth={depth + 1} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const LayersPanel: React.FC = () => {
|
||||
const { nodeIds } = useEditor((state) => {
|
||||
return {
|
||||
nodeIds: Object.keys(state.nodes),
|
||||
};
|
||||
});
|
||||
|
||||
const hasRoot = nodeIds.includes('ROOT');
|
||||
|
||||
if (!hasRoot) {
|
||||
return (
|
||||
<div className="panel-placeholder">
|
||||
No content on canvas
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
margin: '-12px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.5px',
|
||||
color: 'var(--color-text-muted)',
|
||||
borderBottom: '1px solid var(--color-border)',
|
||||
}}
|
||||
>
|
||||
Component Tree
|
||||
</div>
|
||||
<LayerNode nodeId="ROOT" depth={0} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user