feat: initial commit web-cad-neu with docker-compose, frontend and backend
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import React, { useState } from 'react';
|
||||
import type { TreeNode } from '../types/ui.types';
|
||||
|
||||
interface TreeViewProps {
|
||||
nodes: TreeNode[];
|
||||
selectedId?: string | null;
|
||||
onSelect: (id: string) => void;
|
||||
onToggle?: (id: string) => void;
|
||||
renderIcon?: (node: TreeNode) => React.ReactNode;
|
||||
renderActions?: (node: TreeNode) => React.ReactNode;
|
||||
renderDetail?: (node: TreeNode) => React.ReactNode;
|
||||
draggable?: boolean;
|
||||
}
|
||||
|
||||
const TreeView: React.FC<TreeViewProps> = ({
|
||||
nodes,
|
||||
selectedId,
|
||||
onSelect,
|
||||
onToggle,
|
||||
renderIcon,
|
||||
renderActions,
|
||||
renderDetail,
|
||||
draggable = false,
|
||||
}) => {
|
||||
const [expandedSet, setExpandedSet] = useState<Set<string>>(new Set());
|
||||
|
||||
const toggleExpand = (id: string) => {
|
||||
setExpandedSet((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else next.add(id);
|
||||
return next;
|
||||
});
|
||||
onToggle?.(id);
|
||||
};
|
||||
|
||||
const renderNode = (node: TreeNode, level: number): React.ReactNode => {
|
||||
const hasChildren = node.children && node.children.length > 0;
|
||||
const isExpanded = expandedSet.has(node.id) || node.expanded;
|
||||
const isSelected = selectedId === node.id;
|
||||
|
||||
return (
|
||||
<React.Fragment key={node.id}>
|
||||
<div
|
||||
className={`tree-node${isSelected ? ' active' : ''}`}
|
||||
style={{ paddingLeft: `${level * 16 + 8}px` }}
|
||||
onClick={() => onSelect(node.id)}
|
||||
draggable={draggable}
|
||||
>
|
||||
{hasChildren && (
|
||||
<button
|
||||
className="tree-toggle"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleExpand(node.id);
|
||||
}}
|
||||
aria-label={isExpanded ? 'Einklappen' : 'Ausklappen'}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" style={{ transform: isExpanded ? 'rotate(90deg)' : 'none', transition: 'transform 0.15s' }}>
|
||||
<polyline points="9 18 15 12 9 6" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{!hasChildren && <span className="tree-toggle-placeholder" />}
|
||||
{renderIcon && <span className="tree-icon">{renderIcon(node)}</span>}
|
||||
<span className="tree-label">{node.name}</span>
|
||||
{node.count !== undefined && <span className="tree-count">({node.count})</span>}
|
||||
{renderActions && <span className="tree-actions" onClick={(e) => e.stopPropagation()}>{renderActions(node)}</span>}
|
||||
</div>
|
||||
{renderDetail && isSelected && (
|
||||
<div className="tree-detail" style={{ paddingLeft: `${level * 16 + 8}px` }}>
|
||||
{renderDetail(node)}
|
||||
</div>
|
||||
)}
|
||||
{hasChildren && isExpanded && (
|
||||
<div className="tree-children">
|
||||
{node.children!.map((child) => renderNode(child, level + 1))}
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
return <div className="tree-view">{nodes.map((node) => renderNode(node, 0))}</div>;
|
||||
};
|
||||
|
||||
export default TreeView;
|
||||
Reference in New Issue
Block a user