-

Components

-
-
-
- Transformer - One output only -
-
-
- Node - Cable Box or Pole +
+
Objects
+
+ + + +
-
-

Cable Mode

- - +
+
Cables
+
+ + +
+
+ +
+
Tools
+
+ + +
-
@@ -62,6 +97,9 @@
+ +
+ diff --git a/script.js b/script.js index e0233d8..c397a13 100644 --- a/script.js +++ b/script.js @@ -39,18 +39,20 @@ class ObjectFlowDesigner { this.handleContextMenu.bind(this) ); - // Palette events - document.querySelectorAll(".palette-item").forEach((item) => { - item.addEventListener("click", this.handlePaletteClick.bind(this)); + // Icon toolbar events + document.querySelectorAll(".icon-btn[data-type]").forEach((btn) => { + btn.addEventListener("click", this.handleIconClick.bind(this)); }); - // Control buttons - document - .getElementById("connectionBtn") - .addEventListener("click", () => this.setMode("connect")); + // Tool buttons document .getElementById("selectBtn") .addEventListener("click", () => this.setMode("select")); + document + .getElementById("deleteBtn") + .addEventListener("click", () => this.setMode("delete")); + + // Control buttons document .getElementById("clearBtn") .addEventListener("click", this.clearAll.bind(this)); @@ -84,36 +86,51 @@ class ObjectFlowDesigner { document.body.className = `mode-${mode}`; - // Update button states - document - .getElementById("connectionBtn") - .classList.toggle("btn-primary", mode === "connect"); - document - .getElementById("connectionBtn") - .classList.toggle("btn-outline", mode !== "connect"); - document - .getElementById("selectBtn") - .classList.toggle("btn-primary", mode === "select"); - document - .getElementById("selectBtn") - .classList.toggle("btn-outline", mode !== "select"); + // Update tool button states + document.querySelectorAll(".tool-btn").forEach((btn) => { + btn.classList.remove("active"); + }); + + if (mode === "select") { + document.getElementById("selectBtn").classList.add("active"); + } else if (mode === "delete") { + document.getElementById("deleteBtn").classList.add("active"); + } + + // Clear active state from object/cable buttons when switching to tools + if (mode === "select" || mode === "delete") { + document.querySelectorAll(".icon-btn[data-type]").forEach((btn) => { + btn.classList.remove("active"); + }); + } this.render(); } - handlePaletteClick(event) { + handleIconClick(event) { const type = event.currentTarget.dataset.type; - // Check if triangle already exists - if ( - type === "triangle" && - this.objects.some((obj) => obj.type === "triangle") - ) { - this.showError("Only one triangle is allowed!"); - return; - } + // Clear other active states first + document.querySelectorAll(".icon-btn").forEach((btn) => { + btn.classList.remove("active"); + }); - this.createObject(type, 100, 100); + // Set the clicked button as active + event.currentTarget.classList.add("active"); + + if (type === "triangle") { + // Check if triangle already exists + if (this.objects.some((obj) => obj.type === "triangle")) { + alert("Only one transformer is allowed in the system."); + event.currentTarget.classList.remove("active"); + return; + } + this.setMode("place-triangle"); + } else if (type === "box" || type === "pole" || type === "end") { + this.setMode(`place-${type}`); + } else if (type === "underground" || type === "overhead") { + this.setMode(`connect-${type}`); + } } createObject(type, x, y) { @@ -147,21 +164,28 @@ class ObjectFlowDesigner { outputs: [], }, }; - } else { - // Square - Cable box or Pole + } else if (type === "box" || type === "pole" || type === "end") { + // Node types - Cable box, Pole, or End connection + const nodeTypeMap = { + box: "cable_box", + pole: "pole", + end: "end_connection", + }; + obj = { id, - type, + type: "square", // Keep internal type as square for compatibility x: snappedPos.x, y: snappedPos.y, width: 50, height: 50, data: { number: `N${id}`, - name: `Node ${id}`, - nodeType: "cable_box", // cable_box, pole, or end_connection + nodeType: nodeTypeMap[type], boxPoleType: "", // Hand input field - powerKW: 0, // Power rating in kW + consumers3Phase: 0, // Number of 3-phase consumers (7kW each) + consumers1Phase: 0, // Number of 1-phase consumers (3kW each) + customPowerKW: 0, // Additional custom power in kW description: "", metadata: {}, }, @@ -218,6 +242,20 @@ class ObjectFlowDesigner { this.selectedConnection = null; this.updatePropertiesPanel(); } + } else if (this.mode.startsWith("connect-") && clickedObject) { + if (!this.connectionStart) { + // Start connection + this.connectionStart = clickedObject; + } else if (this.connectionStart !== clickedObject) { + // End connection + const cableType = this.mode.replace("connect-", ""); + this.createConnection( + this.connectionStart, + clickedObject, + `${cableType}-single` + ); + this.connectionStart = null; + } } else if (this.mode === "connect" && clickedObject) { if (!this.connectionStart) { // Start connection @@ -227,6 +265,12 @@ class ObjectFlowDesigner { this.createConnection(this.connectionStart, clickedObject); this.connectionStart = null; } + } else if (this.mode === "delete") { + if (clickedObject) { + this.deleteObject(clickedObject.id); + } else if (clickedConnection) { + this.deleteConnection(clickedConnection.id); + } } else { this.selectedObject = null; this.selectedConnection = null; @@ -281,7 +325,18 @@ class ObjectFlowDesigner { } handleClick(event) { - // Handle click events that don't require dragging + const rect = this.canvas.getBoundingClientRect(); + const screenX = event.clientX - rect.left; + const screenY = event.clientY - rect.top; + const { x, y } = this.screenToWorld(screenX, screenY); + + // Handle placement modes + if (this.mode.startsWith("place-")) { + const type = this.mode.replace("place-", ""); + const snappedPos = this.snapToGrid(x, y); + this.createObject(type, snappedPos.x, snappedPos.y); + return; + } } handleWheel(event) { @@ -399,7 +454,7 @@ class ObjectFlowDesigner { }; } - createConnection(fromObj, toObj) { + createConnection(fromObj, toObj, cableType = "YAKY") { // Validate connection rules if (fromObj === toObj) { this.showError("Cannot connect object to itself!"); @@ -460,8 +515,20 @@ class ObjectFlowDesigner { return; } - // Validate overhead cable connections (only to poles or end connections) - // This will be checked later when cable type is selected + // Set cable type based on mode + let defaultCableType = "YAKY"; + if (cableType === "underground-single") { + defaultCableType = "YAKY"; + } else if (cableType === "overhead-single") { + defaultCableType = "AL"; + } + + // Calculate distance between objects + const distance = Math.round( + Math.sqrt( + Math.pow(toObj.x - fromObj.x, 2) + Math.pow(toObj.y - fromObj.y, 2) + ) / 10 // Convert from pixels to meters (rough approximation) + ); const connection = { id: this.nextId++, @@ -469,9 +536,9 @@ class ObjectFlowDesigner { to: toObj, data: { label: `Cable ${this.nextId - 1}`, - cableType: "YAKY", // YAKY, NA2XY-J (ground), AL, AsXSn (overhead) + cableType: defaultCableType, crossSection: 50, // mm² - length: 100, // meters + length: Math.max(10, distance), // minimum 10 meters description: "", metadata: {}, }, @@ -485,6 +552,7 @@ class ObjectFlowDesigner { this.validateCableConnection(connection); this.render(); + return connection; } validateCableConnection(connection) { @@ -537,7 +605,7 @@ class ObjectFlowDesigner { this.ctx.restore(); // Draw connection preview (in screen coordinates) - if (this.mode === "connect" && this.connectionStart) { + if (this.mode.startsWith("connect-") && this.connectionStart) { this.drawConnectionPreview(); } } @@ -711,13 +779,10 @@ class ObjectFlowDesigner { this.ctx.fillText(`#${obj.data.number}`, obj.x + obj.width / 2, yOffset); yOffset += 13; - // 2. Node power (for boxes and end connections) - if (!isPole && obj.data.powerKW > 0) { - this.ctx.fillText( - `${obj.data.powerKW}kW`, - obj.x + obj.width / 2, - yOffset - ); + // 2. Node power (for all node types) + const totalPower = this.calculateNodeTotalPower(obj); + if (totalPower > 0) { + this.ctx.fillText(`${totalPower}kW`, obj.x + obj.width / 2, yOffset); yOffset += 13; } @@ -836,40 +901,25 @@ class ObjectFlowDesigner { textLines.push(`${connection.data.length}m`); if (textLines.length > 0) { - this.ctx.font = "12px Arial"; + this.ctx.font = "11px Arial"; this.ctx.textAlign = "center"; - // Calculate dimensions for background - const lineHeight = 16; - const padding = 5; - let maxWidth = 0; + const lineHeight = 14; - // Measure all text lines to find the maximum width - textLines.forEach((line) => { - const width = this.ctx.measureText(line).width; - if (width > maxWidth) maxWidth = width; - }); - - const totalHeight = textLines.length * lineHeight; - - // Draw background - this.ctx.fillStyle = "rgba(255, 255, 255, 0.95)"; - this.ctx.strokeStyle = "#4a5568"; - this.ctx.lineWidth = 1; - - const bgX = midPoint.x - maxWidth / 2 - padding; - const bgY = midPoint.y - totalHeight / 2 - padding; - const bgWidth = maxWidth + padding * 2; - const bgHeight = totalHeight + padding * 2; - - this.ctx.fillRect(bgX, bgY, bgWidth, bgHeight); - this.ctx.strokeRect(bgX, bgY, bgWidth, bgHeight); - - // Draw text lines - this.ctx.fillStyle = "#2d3748"; + // Draw text lines with subtle outline for visibility textLines.forEach((line, index) => { const yOffset = (index - (textLines.length - 1) / 2) * lineHeight; - this.ctx.fillText(line, midPoint.x, midPoint.y + yOffset + 4); + const textX = midPoint.x; + const textY = midPoint.y + yOffset + 4; + + // Draw text outline (white stroke for visibility) + this.ctx.strokeStyle = "white"; + this.ctx.lineWidth = 3; + this.ctx.strokeText(line, textX, textY); + + // Draw text fill + this.ctx.fillStyle = "#2d3748"; + this.ctx.fillText(line, textX, textY); }); } @@ -1018,34 +1068,57 @@ class ObjectFlowDesigner { panel.innerHTML = `

Node Properties

+ + +
+ + + +
+ + - - - - - + + + - ${ - obj.data.nodeType !== "pole" - ? ` - - - ` - : "" - } + + + + + + + + + +
@@ -1061,11 +1134,14 @@ class ObjectFlowDesigner { }

ID: ${obj.id}

Position: (${Math.round(obj.x)}, ${Math.round(obj.y)})

- ${ - obj.data.nodeType !== "pole" && obj.data.powerKW > 0 - ? `

Power: ${obj.data.powerKW} kW

` - : "" - } +

Total Power: ${this.calculateNodeTotalPower(obj)} kW

+

3-Phase: ${obj.data.consumers3Phase || 0} consumers (${ + (obj.data.consumers3Phase || 0) * 7 + }kW)

+

1-Phase: ${obj.data.consumers1Phase || 0} consumers (${ + (obj.data.consumers1Phase || 0) * 3 + }kW)

+

Custom: ${obj.data.customPowerKW || 0} kW

Inputs: ${obj.connections.inputs.length}${ obj.data.nodeType === "end_connection" ? "" : "/1" }

@@ -1086,17 +1162,22 @@ class ObjectFlowDesigner { document.getElementById("objNumber").addEventListener("input", (e) => { obj.data.number = e.target.value; }); - document.getElementById("objName").addEventListener("input", (e) => { - obj.data.name = e.target.value; - }); - document.getElementById("objNodeType").addEventListener("change", (e) => { - obj.data.nodeType = e.target.value; - // Reset power for poles - if (e.target.value === "pole") { - obj.data.powerKW = 0; - } - this.updatePropertiesPanel(); // Refresh to update options - this.render(); // Re-render to update visual representation + + // Add event listeners for node type buttons + document.querySelectorAll(".node-type-btn").forEach((btn) => { + btn.addEventListener("click", (e) => { + const newType = e.currentTarget.dataset.type; + obj.data.nodeType = newType; + + // Update active button styling + document + .querySelectorAll(".node-type-btn") + .forEach((b) => b.classList.remove("active")); + e.currentTarget.classList.add("active"); + + this.updatePropertiesPanel(); // Refresh to update options + this.render(); // Re-render to update visual representation + }); }); document .getElementById("objBoxPoleType") @@ -1105,11 +1186,33 @@ class ObjectFlowDesigner { this.render(); // Re-render to update visual representation }); - // Only add power listener if the field exists (not for poles) - const powerField = document.getElementById("objPowerKW"); - if (powerField) { - powerField.addEventListener("input", (e) => { - obj.data.powerKW = parseFloat(e.target.value) || 0; + // Add consumer and power listeners for all node types + const consumers3PhaseField = + document.getElementById("objConsumers3Phase"); + if (consumers3PhaseField) { + consumers3PhaseField.addEventListener("input", (e) => { + obj.data.consumers3Phase = parseInt(e.target.value) || 0; + this.updatePropertiesPanel(); // Refresh to update calculated power display + this.render(); // Re-render to update visual representation + }); + } + + const consumers1PhaseField = + document.getElementById("objConsumers1Phase"); + if (consumers1PhaseField) { + consumers1PhaseField.addEventListener("input", (e) => { + obj.data.consumers1Phase = parseInt(e.target.value) || 0; + this.updatePropertiesPanel(); // Refresh to update calculated power display + this.render(); // Re-render to update visual representation + }); + } + + const customPowerField = document.getElementById("objCustomPowerKW"); + if (customPowerField) { + customPowerField.addEventListener("input", (e) => { + obj.data.customPowerKW = parseFloat(e.target.value) || 0; + this.updatePropertiesPanel(); // Refresh to update calculated power display + this.render(); // Re-render to update visual representation }); } @@ -1283,24 +1386,72 @@ class ObjectFlowDesigner { } showTooltip(obj, screenX, screenY) { - const tooltip = document.getElementById("tooltip"); + let tooltip = document.getElementById("tooltip"); + + // If tooltip doesn't exist or is not properly positioned, recreate it + if (!tooltip) { + tooltip = document.createElement("div"); + tooltip.id = "tooltip"; + tooltip.className = "tooltip"; + document.body.appendChild(tooltip); + } if (obj) { tooltip.style.display = "block"; - tooltip.style.left = screenX + 10 + "px"; - tooltip.style.top = screenY - 30 + "px"; - tooltip.textContent = `${obj.data.name} (${obj.type})`; + + // Convert canvas coordinates to viewport coordinates + const canvasRect = this.canvas.getBoundingClientRect(); + const viewportX = canvasRect.left + screenX; + const viewportY = canvasRect.top + screenY; + + // Position tooltip with some offset and ensure it doesn't go off screen + const leftPos = Math.min(viewportX + 15, window.innerWidth - 200); + const topPos = Math.max(viewportY - 35, 10); + + // Force all positioning styles + tooltip.style.position = "fixed"; + tooltip.style.left = leftPos + "px"; + tooltip.style.top = topPos + "px"; + tooltip.style.zIndex = "999999"; + tooltip.style.pointerEvents = "none"; + tooltip.style.background = "rgba(0, 0, 0, 0.9)"; + tooltip.style.color = "white"; + tooltip.style.padding = "0.5rem"; + tooltip.style.borderRadius = "4px"; + tooltip.style.fontSize = "0.8rem"; + tooltip.textContent = `${obj.data.number || obj.data.name || "Object"} (${ + obj.type === "triangle" ? "Transformer" : "Node" + })`; } else { // Check if hovering over a connection const worldPos = this.screenToWorld(screenX, screenY); const connection = this.getConnectionAt(worldPos.x, worldPos.y); if (connection) { tooltip.style.display = "block"; - tooltip.style.left = screenX + 10 + "px"; - tooltip.style.top = screenY - 30 + "px"; + + // Convert canvas coordinates to viewport coordinates + const canvasRect = this.canvas.getBoundingClientRect(); + const viewportX = canvasRect.left + screenX; + const viewportY = canvasRect.top + screenY; + + // Position tooltip with some offset and ensure it doesn't go off screen + const leftPos = Math.min(viewportX + 15, window.innerWidth - 200); + const topPos = Math.max(viewportY - 35, 10); + + // Force all positioning styles + tooltip.style.position = "fixed"; + tooltip.style.left = leftPos + "px"; + tooltip.style.top = topPos + "px"; + tooltip.style.zIndex = "999999"; + tooltip.style.pointerEvents = "none"; + tooltip.style.background = "rgba(0, 0, 0, 0.9)"; + tooltip.style.color = "white"; + tooltip.style.padding = "0.5rem"; + tooltip.style.borderRadius = "4px"; + tooltip.style.fontSize = "0.8rem"; tooltip.textContent = `${connection.data.label || "Connection"} (${ - connection.from.data.name - } → ${connection.to.data.name})`; + connection.from.data.number || connection.from.data.name || "From" + } → ${connection.to.data.number || connection.to.data.name || "To"})`; } else { tooltip.style.display = "none"; } @@ -1481,7 +1632,7 @@ class ObjectFlowDesigner { calculateDownstreamPower(node) { // Calculate total power of this node and all downstream nodes - let totalPower = node.data.powerKW || 0; + let totalPower = this.calculateNodeTotalPower(node); const visited = new Set(); const calculateRecursive = (currentNode) => { @@ -1491,7 +1642,7 @@ class ObjectFlowDesigner { let downstreamPower = 0; for (const connection of currentNode.connections.outputs) { const toNode = connection.to; - downstreamPower += toNode.data.powerKW || 0; + downstreamPower += this.calculateNodeTotalPower(toNode); downstreamPower += calculateRecursive(toNode); } return downstreamPower; @@ -1500,6 +1651,19 @@ class ObjectFlowDesigner { totalPower += calculateRecursive(node); return totalPower; } + + // Calculate total power for a node based on consumers and custom power + calculateNodeTotalPower(node) { + if (node.type === "triangle") { + return 0; // Transformers don't have power consumption + } + + const power3Phase = (node.data.consumers3Phase || 0) * 7; // 7kW per 3-phase consumer + const power1Phase = (node.data.consumers1Phase || 0) * 3; // 3kW per 1-phase consumer + const customPower = node.data.customPowerKW || 0; + + return power3Phase + power1Phase + customPower; + } } // Initialize the application diff --git a/styles.css b/styles.css index 376aac0..eee434e 100644 --- a/styles.css +++ b/styles.css @@ -103,79 +103,297 @@ body { } .toolbar { - width: 250px; + width: 100px; background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); - padding: 1.5rem; + padding: 1rem 0.75rem; border-right: 1px solid rgba(255, 255, 255, 0.2); - overflow-y: auto; + overflow: visible; + display: flex; + flex-direction: column; + gap: 1.5rem; } -.toolbar h3, -.toolbar h4 { - color: #4a5568; - margin-bottom: 1rem; -} - -.object-palette { - margin-bottom: 2rem; -} - -.palette-item { +.toolbar-section { display: flex; flex-direction: column; align-items: center; - padding: 1rem; - margin-bottom: 1rem; + gap: 0.5rem; +} + +.toolbar-label { + font-size: 0.7rem; + font-weight: 600; + color: #718096; + text-transform: uppercase; + letter-spacing: 0.5px; + writing-mode: horizontal-tb; + text-align: center; + line-height: 1.2; +} + +.icon-toolbar { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; + width: 100%; +} + +.icon-btn { + width: 44px; + height: 44px; + border: none; + border-radius: 8px; background: white; - border-radius: 12px; cursor: pointer; + display: flex; + align-items: center; + justify-content: center; transition: all 0.3s ease; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); border: 2px solid transparent; + position: relative; } -.palette-item:hover { - transform: translateY(-3px); +.icon-btn:hover { + transform: translateY(-2px); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15); border-color: #4299e1; } -.palette-item span { - font-weight: 600; - color: #4a5568; - margin: 0.5rem 0; +.icon-btn.active { + background: #4299e1; + border-color: #3182ce; + transform: translateY(-1px); } -.palette-item small { - color: #718096; - font-size: 0.8rem; - text-align: center; +.icon-btn.active:hover { + background: #3182ce; } -.triangle-preview { +/* Tooltip enhancements */ +.icon-btn::after { + content: attr(title); + position: absolute; + left: 100%; + top: 50%; + transform: translateY(-50%); + margin-left: 8px; + padding: 6px 8px; + background: rgba(0, 0, 0, 0.8); + color: white; + font-size: 0.7rem; + border-radius: 4px; + white-space: nowrap; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s ease; + z-index: 1000; +} + +.icon-btn:hover::after { + opacity: 1; +} + +/* Object Icons */ +.icon-triangle { width: 0; height: 0; - border-left: 15px solid transparent; - border-right: 15px solid transparent; - border-bottom: 25px solid #48bb78; + border-left: 12px solid transparent; + border-right: 12px solid transparent; + border-bottom: 20px solid #48bb78; } -.square-preview { - width: 30px; - height: 30px; +.icon-btn.active .icon-triangle { + border-bottom-color: white; +} + +.icon-box { + width: 24px; + height: 24px; background: #4299e1; - border-radius: 4px; + border-radius: 3px; } -.connection-mode { - border-top: 1px solid rgba(0, 0, 0, 0.1); - padding-top: 1rem; +.icon-btn.active .icon-box { + background: white; } -.connection-mode .btn { +.icon-pole { + width: 4px; + height: 28px; + background: #8b4513; + border-radius: 2px; + position: relative; +} + +.icon-pole::before { + content: ""; + position: absolute; + top: 2px; + left: -6px; + width: 16px; + height: 2px; + background: #4a5568; + border-radius: 1px; +} + +.icon-pole::after { + content: ""; + position: absolute; + top: 6px; + left: -6px; + width: 16px; + height: 2px; + background: #4a5568; + border-radius: 1px; +} + +.icon-btn.active .icon-pole { + background: white; +} + +.icon-btn.active .icon-pole::before, +.icon-btn.active .icon-pole::after { + background: white; +} + +.icon-end { + width: 20px; + height: 20px; + border: 3px solid #e53e3e; + border-radius: 50%; + position: relative; +} + +.icon-end::before { + content: ""; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 8px; + height: 8px; + background: #e53e3e; + border-radius: 50%; +} + +.icon-btn.active .icon-end { + border-color: white; +} + +.icon-btn.active .icon-end::before { + background: white; +} + +/* Cable Icons */ +.icon-underground { + width: 24px; + height: 3px; + background: #2d3748; + border-radius: 1.5px; + position: relative; +} + +.icon-underground::before { + content: ""; + position: absolute; + top: -3px; + left: 0; width: 100%; - margin-bottom: 0.5rem; + height: 1px; + background: repeating-linear-gradient( + to right, + #2d3748 0px, + #2d3748 2px, + transparent 2px, + transparent 4px + ); +} + +.icon-btn.active .icon-underground { + background: white; +} + +.icon-btn.active .icon-underground::before { + background: repeating-linear-gradient( + to right, + white 0px, + white 2px, + transparent 2px, + transparent 4px + ); +} + +.icon-overhead { + width: 24px; + height: 2px; + background: #2d3748; + border-radius: 1px; +} + +.icon-btn.active .icon-overhead { + background: white; +} + +/* Tool Icons */ +.icon-select { + width: 20px; + height: 20px; + border: 2px solid #4a5568; + border-radius: 2px; + position: relative; +} + +.icon-select::before { + content: ""; + position: absolute; + top: 2px; + left: 2px; + width: 8px; + height: 8px; + border: 1px solid #4a5568; + border-radius: 1px; +} + +.icon-btn.active .icon-select { + border-color: white; +} + +.icon-btn.active .icon-select::before { + border-color: white; +} + +.icon-delete { + width: 20px; + height: 20px; + position: relative; +} + +.icon-delete::before, +.icon-delete::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + width: 20px; + height: 2px; + background: #e53e3e; + border-radius: 1px; +} + +.icon-delete::before { + transform: translate(-50%, -50%) rotate(45deg); +} + +.icon-delete::after { + transform: translate(-50%, -50%) rotate(-45deg); +} + +.icon-btn.active .icon-delete::before, +.icon-btn.active .icon-delete::after { + background: white; } .canvas-container { @@ -249,22 +467,38 @@ body { } .tooltip { - position: absolute; - background: rgba(0, 0, 0, 0.8); + position: fixed; + background: rgba(0, 0, 0, 0.9); color: white; padding: 0.5rem; border-radius: 4px; font-size: 0.8rem; pointer-events: none; - z-index: 1000; + z-index: 999999; display: none; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.2); + max-width: 200px; + word-wrap: break-word; } -.mode-connecting #canvas { +.mode-connect-underground #canvas, +.mode-connect-overhead #canvas { cursor: crosshair; } -.mode-selecting #canvas { +.mode-place-triangle #canvas, +.mode-place-box #canvas, +.mode-place-pole #canvas, +.mode-place-end #canvas { + cursor: copy; +} + +.mode-delete #canvas { + cursor: not-allowed; +} + +.mode-select #canvas { cursor: default; } @@ -362,6 +596,74 @@ body { border-radius: 4px; } +/* Node Type Selector Styles */ +.node-type-selector { + display: flex; + gap: 8px; + margin: 8px 0; +} + +.node-type-btn { + display: flex; + flex-direction: column; + align-items: center; + padding: 8px 12px; + border: 2px solid #e2e8f0; + border-radius: 8px; + background: #f7fafc; + cursor: pointer; + transition: all 0.2s ease; + flex: 1; + min-height: 60px; + font-family: inherit; +} + +.node-type-btn:hover { + border-color: #4299e1; + background: #ebf8ff; + transform: translateY(-1px); +} + +.node-type-btn.active { + border-color: #3182ce; + background: #3182ce; + color: white; +} + +.node-type-btn .node-icon { + font-size: 16px; + margin-bottom: 4px; + display: block; +} + +.node-type-btn .node-label { + font-size: 11px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.node-type-btn.active .node-icon, +.node-type-btn.active .node-label { + color: white; +} + +/* Different colors for each node type when active */ +.node-type-btn[data-type="cable_box"].active { + border-color: #3182ce; + background: #3182ce; +} + +.node-type-btn[data-type="pole"].active { + border-color: #4a5568; + background: #4a5568; +} + +.node-type-btn[data-type="end_connection"].active { + border-color: #e53e3e; + background: #e53e3e; +} + /* Responsive design */ @media (max-width: 1024px) { .main-content {