138 lines
3.9 KiB
TypeScript
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)' }}>
|
|
▼
|
|
</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>
|
|
);
|
|
};
|