Files
site-builder/craft/src/panels/left/LayersPanel.tsx
T

138 lines
3.9 KiB
TypeScript

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.props?.aiName as string) || node.data.displayName || (node.data.type as any)?.resolvedName || 'Node';
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)' }}>
&#9660;
</span>
)}
{allChildren.length === 0 && (
<span style={{ marginRight: 4, fontSize: 8, color: 'transparent' }}>
&#9660;
</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>
);
};