JSFiddle - React, Tailwind, and code Playground

by velo_ninja

HTML

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Diagram Editor</title>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/konva.min.js"></script>
  <style>
    /* Reset & Base */
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { display: flex; height: 100vh; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #eef1f5; }

    /* Toolbar */
    #toolbar {
      width: 260px;
      background: #2c3e50;
      padding: 20px;
      display: flex;
      flex-direction: column;
      gap: 15px;
      box-shadow: 2px 0 10px rgba(0,0,0,0.1);
    }
    #toolbar button {
      background: #3498db;
      color: #fff;
      border: none;
      border-radius: 5px;
      padding: 12px;
      font-size: 14px;
      cursor: pointer;
      transition: background 0.3s ease, transform 0.2s ease;
    }
    #toolbar button:hover {
      background: #5dade2;
      transform: translateY(-2px);
    }

    /* Canvas Container */
    #container {
      flex: 1;
      margin: 20px;
      background: #fff;
      border-radius: 8px;
      box-shadow: 0 4px 20px rgba(0,0,0,0.1);
      position: relative;
      overflow: hidden;
    }

    /* Context Menu */
    #context-menu {
      position: absolute;
      display: none;
      background: #fff;
      border-radius: 5px;
      border: 1px solid #ccc;
      box-shadow: 0 4px 12px rgba(0,0,0,0.15);
      z-index: 1000;
      min-width: 180px;
    }
    #context-menu ul { list-style: none; }
    #context-menu li {
      padding: 10px 14px;
      font-size: 13px;
      color: #333;
      cursor: pointer;
      transition: background 0.2s ease;
    }
    #context-menu li:hover { background: #f4f6fa; }

    /* Konva Canvas Styling */
    #container canvas {
      border-radius: 8px;
      background: #fcfcfc;
    }
  </style>
</head>
<body>
  <div id="toolbar">
    <button id="addHopper">Add...

CSS

/* Global styles */
body {
  margin: 0;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  background-color: #f4f7fc;
  display: flex;
  height: 100vh;
  color: #333;
}

/* Toolbar styles */
#toolbar {
  width: 240px;
  background: #2e3b4e;
  color: white;
  padding: 20px;
  box-sizing: border-box;
  border-right: 2px solid #2c3e50;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  align-items: center;
}

/* Toolbar buttons */
#toolbar button {
  background-color: #4caf50;
  color: white;
  font-size: 16px;
  padding: 12px 20px;
  margin: 10px 0;
  border: none;
  border-radius: 5px;
  width: 100%;
  cursor: pointer;
  transition: background-color 0.3s ease;
  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
}

#toolbar button:hover {
  background-color: #45a049;
}

#toolbar button:active {
  background-color: #388e3c;
}

/* Container for the canvas area */
#container {
  flex-grow: 1;
  position: relative;
  padding: 25px;
  margin-left: 25px;
  margin-right: 25px;
  margin-top: 25px;
  margin-bottom: 25px;
  background-color: #fff;
  border-radius: 8px;
  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
  overflow: hidden;
}

/* Context menu */
#context-menu {
  position: absolute;
  display: none;
  background-color: #fff;
  border-radius: 8px;
  border: 1px solid #ccc;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  z-index: 10;
  width: 200px;
}

#context-menu ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

#context-menu li {
  padding: 10px;
  cursor: pointer;
  font-size: 14px;
  color: #333;
  transition: background-color 0.3s ease, padding 0.2s ease;
}

#context-menu li:hover {
  background-color: #f4f4f4;
  padding-left: 15px;
}

/* Animation for the context menu */
@keyframes menuSlideIn {
  from {
    transform: translateX(20px);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}

#context-menu {
  animation: menuSlideIn 0.3s ease-out;
}

/* A more modern and clean scrollbar style...

JavaScript

const stage = new Konva.Stage({
  container: 'container',
  width: window.innerWidth - 200,
  height: window.innerHeight
});

const wireLayer = new Konva.Layer();
const elementLayer = new Konva.Layer();
stage.add(wireLayer);
stage.add(elementLayer);

let elements = [];
let wires = [];
let wireStart = null;
let contextMenuTarget = null;

// Add element function to create shapes with labels
function addElement(type) {
  const group = new Konva.Group({
    x: 100 + Math.random() * 300,
    y: 100 + Math.random() * 200,
    draggable: true
  });

  let shape;
  switch(type) {
    case 'hopper':
      shape = new Konva.Rect({ width: 100, height: 50, fill: '#ffa', stroke: 'black', strokeWidth: 2 });
      break;
    case 'control':
      shape = new Konva.Circle({ radius: 30, fill: '#aff', stroke: 'black', strokeWidth: 2 });
      break;
    case 'vacuum':
      shape = new Konva.RegularPolygon({ sides: 3, radius: 40, fill: '#faf', stroke: 'black', strokeWidth: 2 });
      break;
    default:
      shape = new Konva.Rect({ width: 100, height: 50, fill: '#ddd', stroke: 'black', strokeWidth: 2 });
  }

  const label = new Konva.Text({ text: type, fontSize: 14, x: 0, y: shape.height() + 10, align: 'center' });
  label.x((shape.width() - label.width()) / 2);  // Center label horizontally

  group.add(shape);
  group.add(label);

  // Create the comment but keep it invisible until hover
  let comment = null;

  // Show comment on hover
  group.on('mouseenter', () => {
    // Create the comment only on hover (if it doesn't exist yet)
    if (!comment) {
      comment = new Konva.Text({
        text: "Comment",  // Placeholder comment text
        fontSize: 14,
        fill: 'blue',
        opacity: 0,  // Hidden by default
        x: 0,
        y: shape.height() + 10
      });
      group.add(comment);
    }

    // Show the comment
    comment.opacity(1); // Show comment on hover
    group.getLayer().batchDraw(); // Redraw the layer

    // Change cursor to hand when...