*, *::before, *::after {
box-sizing: border-box;
}
body {
font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
margin: 0;
background-color: #020617;
background-image: radial-gradient(circle, #1e293b 1px, transparent 1px);
background-size: 30px 30px;
min-height: 100vh;
overflow: hidden;
}
.workspace {
position: relative;
width: 100vw;
height: 100vh;
}
.svg-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
}
.flow-line {
fill: none;
stroke: #0ea5e9;
stroke-width: 3;
stroke-dasharray: 8 8;
animation: dashAnim 1s linear infinite;
filter: drop-shadow(0 0 6px rgba(14, 165, 233, 0.6));
}
@keyframes dashAnim {
to { stroke-dashoffset: -16; }
}
.flow-node {
position: absolute;
width: 240px;
background: rgba(15, 23, 42, 0.9);
backdrop-filter: blur(12px);
border: 1px solid rgba(14, 165, 233, 0.4);
border-top: 4px solid #0ea5e9;
border-radius: 12px;
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.5);
z-index: 10;
cursor: grab;
user-select: none;
display: flex;
flex-direction: column;
transition: box-shadow 0.2s, border-color 0.2s;
}
.flow-node:hover {
border-color: #38bdf8;
}
.flow-node:active {
cursor: grabbing;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.8);
}
.node-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: rgba(255, 255, 255, 0.05);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.editable {
color: #f8fafc;
font-weight: 600;
outline: none;
padding: 4px;
border-radius: 4px;
border: 1px solid transparent;
transition: border-color 0.2s, background 0.2s;
cursor: text;
flex-grow: 1;
}
.editable:focus {
border-color: rgba(14, 165, 233, 0.5);
background: rgba(0, 0, 0, 0.3);
}
.delete-btn {
background: transparent;
color: #94a3b8;
border: none;
font-size: 1.25rem;
cursor: pointer;
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
justify-content: center;
align-items: center;
transition: background 0.2s, color 0.2s;
margin-left: 8px;
}
.delete-btn:hover {
background: #ef4444;
color: #ffffff;
}
.node-body {
padding: 16px;
color: #cbd5e1;
font-size: 0.95rem;
font-weight: 400;
line-height: 1.5;
margin-bottom: 8px; /* Room for the add button */
}
.add-node-btn {
position: absolute;
bottom: -16px;
left: 50%;
transform: translateX(-50%);
width: 32px;
height: 32px;
border-radius: 50%;
background: #10b981;
color: #ffffff;
border: 3px solid #0f172a;
display: flex;
justify-content: center;
align-items: center;
font-size: 1.25rem;
font-weight: bold;
cursor: pointer;
box-shadow: 0 4px 10px rgba(0,0,0,0.4);
transition: background 0.2s, transform 0.2s;
z-index: 20;
}
.add-node-btn:hover {
background: #059669;
transform: translateX(-50%) scale(1.1);
}
const workspace = document.getElementById("workspace");
const svgLayer = document.getElementById("svg-layer");
let activeNode = null;
let offsetX = 0;
let offsetY = 0;
let nodeCounter = 1;
function drawLines() {
while (svgLayer.firstChild) {
svgLayer.removeChild(svgLayer.firstChild);
}
const nodes = document.querySelectorAll(".flow-node");
for (let i = 0; i < nodes.length; i++) {
const child = nodes.item(i);
const parentId = child.getAttribute("data-parent");
if (parentId) {
const parent = document.getElementById(parentId);
if (parent) {
const r1 = parent.getBoundingClientRect();
const r2 = child.getBoundingClientRect();
// Bottom center of parent
const c1x = r1.left + (r1.width / 2);
const c1y = r1.top + r1.height;
// Top center of child
const c2x = r2.left + (r2.width / 2);
const c2y = r2.top;
// Control points for smooth bezier curve
const cp1x = c1x;
const cp1y = c1y + 50;
const cp2x = c2x;
const cp2y = c2y - 50;
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("class", "flow-line");
path.setAttribute("d", "M " + c1x + " " + c1y + " C " + cp1x + " " + cp1y + ", " + cp2x + " " + cp2y + ", " + c2x + " " + c2y);
svgLayer.appendChild(path);
}
}
}
}
function deleteNodeAndChildren(nodeId) {
const node = document.getElementById(nodeId);
if (node) {
node.remove();
}
// Recursively find and delete any children attached to this node
const nodes = document.querySelectorAll(".flow-node");
for (let i = 0; i < nodes.length; i++) {
const child = nodes.item(i);
if (child.getAttribute("data-parent") === nodeId) {
deleteNodeAndChildren(child.id);
}
}
}
function startDrag(e, node) {
// Prevent dragging when clicking interactive elements
if (e.target.classList.contains("editable") || e.target.classList.contains("delete-btn") || e.target.classList.contains("add-node-btn")) {
return;
}
activeNode = node;
const rect = node.getBoundingClientRect();
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
node.style.zIndex = 100;
}
function attachListeners(node) {
node.addEventListener("pointerdown", function(e) {
startDrag(e, node);
});
const delBtn = node.querySelector(".delete-btn");
if (delBtn) {
delBtn.addEventListener("click", function() {
deleteNodeAndChildren(node.id);
drawLines();
});
}
const addBtn = node.querySelector(".add-node-btn");
if (addBtn) {
addBtn.addEventListener("click", function() {
createNewNode(node.id);
});
}
const editables = node.querySelectorAll(".editable");
for (let i = 0; i < editables.length; i++) {
editables.item(i).addEventListener("pointerdown", function(e) {
e.stopPropagation();
});
editables.item(i).addEventListener("input", function() {
drawLines();
});
}
}
function createNewNode(parentId) {
nodeCounter++;
const parent = document.getElementById(parentId);
const pr = parent.getBoundingClientRect();
const newNode = document.createElement("div");
const newId = "node-" + nodeCounter;
newNode.id = newId;
newNode.className = "flow-node";
newNode.setAttribute("data-parent", parentId);
// Offset new node slightly below and randomly to the side of the parent
const newX = pr.left + (Math.random() * 100 - 50);
const newY = pr.top + 150;
newNode.style.left = newX + "px";
newNode.style.top = newY + "px";
const header = document.createElement("div");
header.className = "node-header";
const title = document.createElement("div");
title.className = "editable";
title.setAttribute("contenteditable", "true");
title.innerText = "Step " + nodeCounter;
const delBtn = document.createElement("button");
delBtn.className = "delete-btn";
delBtn.innerText = "×";
header.appendChild(title);
header.appendChild(delBtn);
const bodyDiv = document.createElement("div");
bodyDiv.className = "node-body editable";
bodyDiv.setAttribute("contenteditable", "true");
bodyDiv.innerText = "Enter details here...";
const addBtn = document.createElement("button");
addBtn.className = "add-node-btn";
addBtn.innerText = "+";
newNode.appendChild(header);
newNode.appendChild(bodyDiv);
newNode.appendChild(addBtn);
workspace.appendChild(newNode);
attachListeners(newNode);
drawLines();
}
// Attach listeners to the initial root node
const rootNode = document.getElementById("node-root");
attachListeners(rootNode);
window.addEventListener("pointermove", function(e) {
if (!activeNode) return;
const x = e.clientX - offsetX;
const y = e.clientY - offsetY;
activeNode.style.left = x + "px";
activeNode.style.top = y + "px";
drawLines();
});
window.addEventListener("pointerup", function() {
if (activeNode) {
activeNode.style.zIndex = 10;
activeNode = null;
}
});
window.addEventListener("resize", drawLines);
setTimeout(drawLines, 100);