import React, { useState, useEffect } from "react"; import { Box, Typography, Tabs, Tab, TextField, MenuItem, Button, Slider, IconButton, Tooltip } from "@mui/material"; import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import ContentPasteIcon from "@mui/icons-material/ContentPaste"; import RestoreIcon from "@mui/icons-material/Restore"; import { SketchPicker } from "react-color"; const SIDEBAR_WIDTH = 400; const DEFAULT_EDGE_STYLE = { type: "bezier", animated: true, style: { strokeDasharray: "6 3", stroke: "#58a6ff", strokeWidth: 1 }, label: "", labelStyle: { fill: "#fff", fontWeight: "bold" }, labelBgStyle: { fill: "#2c2c2c", fillOpacity: 0.85, rx: 16, ry: 16 }, labelBgPadding: [8, 4], }; let globalEdgeClipboard = null; function clone(obj) { return JSON.parse(JSON.stringify(obj)); } export default function Context_Menu_Sidebar({ open, onClose, edge, updateEdge, }) { const [activeTab, setActiveTab] = useState(0); const [editState, setEditState] = useState(() => (edge ? clone(edge) : {})); const [colorPicker, setColorPicker] = useState({ field: null, anchor: null }); useEffect(() => { if (edge && edge.id !== editState.id) setEditState(clone(edge)); // eslint-disable-next-line }, [edge]); const handleChange = (field, value) => { setEditState((prev) => { const updated = { ...prev }; if (field === "label") updated.label = value; else if (field === "labelStyle.fill") updated.labelStyle = { ...updated.labelStyle, fill: value }; else if (field === "labelBgStyle.fill") updated.labelBgStyle = { ...updated.labelBgStyle, fill: value }; else if (field === "labelBgStyle.rx") updated.labelBgStyle = { ...updated.labelBgStyle, rx: value, ry: value }; else if (field === "labelBgPadding") updated.labelBgPadding = value; else if (field === "labelBgStyle.fillOpacity") updated.labelBgStyle = { ...updated.labelBgStyle, fillOpacity: value }; else if (field === "type") updated.type = value; else if (field === "animated") updated.animated = value; else if (field === "style.stroke") updated.style = { ...updated.style, stroke: value }; else if (field === "style.strokeDasharray") updated.style = { ...updated.style, strokeDasharray: value }; else if (field === "style.strokeWidth") updated.style = { ...updated.style, strokeWidth: value }; else if (field === "labelStyle.fontWeight") updated.labelStyle = { ...updated.labelStyle, fontWeight: value }; else updated[field] = value; if (field === "style.strokeDasharray") { if (value === "") { updated.animated = false; updated.style = { ...updated.style, strokeDasharray: "" }; } else { updated.animated = true; updated.style = { ...updated.style, strokeDasharray: value }; } } updateEdge({ ...updated, id: prev.id }); return updated; }); }; // Color Picker with right alignment const openColorPicker = (field, event) => { setColorPicker({ field, anchor: event.currentTarget }); }; const closeColorPicker = () => { setColorPicker({ field: null, anchor: null }); }; const handleColorChange = (color) => { handleChange(colorPicker.field, color.hex); closeColorPicker(); }; // Reset, Copy, Paste logic const handleReset = () => { setEditState(clone({ ...DEFAULT_EDGE_STYLE, id: edge.id })); updateEdge({ ...DEFAULT_EDGE_STYLE, id: edge.id }); }; const handleCopy = () => { globalEdgeClipboard = clone(editState); }; const handlePaste = () => { if (globalEdgeClipboard) { setEditState(clone({ ...globalEdgeClipboard, id: edge.id })); updateEdge({ ...globalEdgeClipboard, id: edge.id }); } }; const renderColorButton = (label, field, value) => ( {colorPicker.field === field && ( )} ); // Label tab const renderLabelTab = () => ( Label handleChange("label", e.target.value)} sx={{ mb: 2, input: { color: "#fff", bgcolor: "#1e1e1e", fontSize: "0.95rem" }, "& fieldset": { borderColor: "#444" }, }} /> Text Color {renderColorButton("Label Text Color", "labelStyle.fill", editState.labelStyle?.fill || "#fff")} Background {renderColorButton("Label Background Color", "labelBgStyle.fill", editState.labelBgStyle?.fill || "#2c2c2c")} Padding { const val = e.target.value.split(",").map(x => parseInt(x.trim())).filter(x => !isNaN(x)); if (val.length === 2) handleChange("labelBgPadding", val); }} sx={{ width: 80, input: { color: "#fff", bgcolor: "#1e1e1e", fontSize: "0.95rem" } }} /> Background Style = 11 ? "rounded" : "square"} onChange={e => { handleChange("labelBgStyle.rx", e.target.value === "rounded" ? 11 : 0); }} sx={{ width: 150, bgcolor: "#1e1e1e", "& .MuiSelect-select": { color: "#fff" } }} > Rounded Square Background Opacity handleChange("labelBgStyle.fillOpacity", v)} sx={{ width: 100, ml: 2 }} /> handleChange("labelBgStyle.fillOpacity", parseFloat(e.target.value) || 0)} sx={{ width: 60, ml: 2, input: { color: "#fff", bgcolor: "#1e1e1e", fontSize: "0.95rem" } }} /> ); const renderStyleTab = () => ( Edge Style handleChange("type", e.target.value)} sx={{ width: 200, bgcolor: "#1e1e1e", "& .MuiSelect-select": { color: "#fff" } }} > Step Curved (Bezier) Straight Smoothstep Edge Animation { const val = e.target.value; handleChange("style.strokeDasharray", val === "dashes" ? "6 3" : val === "dots" ? "2 4" : "" ); }} sx={{ width: 200, bgcolor: "#1e1e1e", "& .MuiSelect-select": { color: "#fff" } }} > Dashes Dots Solid Color {renderColorButton("Edge Color", "style.stroke", editState.style?.stroke || "#58a6ff")} Edge Width handleChange("style.strokeWidth", v)} sx={{ width: 100, ml: 2 }} /> handleChange("style.strokeWidth", parseInt(e.target.value) || 1)} sx={{ width: 60, ml: 2, input: { color: "#fff", bgcolor: "#1e1e1e", fontSize: "0.95rem" } }} /> ); // Always render the sidebar for animation! if (!edge) return null; return ( <> {/* Overlay */} {/* Sidebar */} e.stopPropagation()} > Edit Edge Properties setActiveTab(v)} variant="fullWidth" textColor="inherit" TabIndicatorProps={{ style: { backgroundColor: "#ccc" } }} sx={{ borderTop: "1px solid #333", borderBottom: "1px solid #333", minHeight: "36px", height: "36px" }} > {/* Main fields scrollable */} {activeTab === 0 && renderLabelTab()} {activeTab === 1 && renderStyleTab()} {/* Sticky footer bar */} ); }