4 Commits

Author SHA1 Message Date
Eliad Shahar
f8639776c9 Merge pull request #104 from CalcsLive/add-calcslive-workflow
Add CalcsLive custom node workflow - unit-aware calculations demo
2025-09-28 16:49:44 +03:00
Eliad Shahar
67a5bb92c5 Merge pull request #103 from rafaelkerni/feat--add-zoom-to-diagram
feat: add zoom to diagram
2025-09-28 16:49:18 +03:00
e3d
54688735f7 Add CalcsLive custom node workflow - engineering calculations demo
- Showcases @calcslive/n8n-nodes-calcslive custom node capabilities
- Demonstrates cylinder geometry and mass calculations
- Includes calculation chaining and email reporting
- Template for engineering automation workflows
- Add .e3d/ to .gitignore for development isolation

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-27 12:15:55 -07:00
Rafael Kerni
5ce4fd4ae1 feat: add zoom to diagram 2025-09-22 17:27:30 -03:00
4 changed files with 746 additions and 17 deletions

5
.gitignore vendored
View File

@@ -90,4 +90,7 @@ package-lock.json
.python-version
# Claude Code local settings (created during development)
.claude/settings.local.json
.claude/settings.local.json
# E3D development directory
.e3d/

View File

@@ -529,12 +529,76 @@
border-radius: 0.5rem;
padding: 1rem;
text-align: center;
overflow-x: auto;
overflow: visible;
min-height: 300px;
}
.mermaid svg {
max-width: 100%;
max-width: none;
height: auto;
transition: transform 0.2s ease;
}
.diagram-container {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 0.5rem;
padding: 1rem;
text-align: center;
overflow: hidden;
height: 500px;
position: relative;
cursor: grab;
user-select: none;
}
.diagram-container.dragging {
cursor: grabbing;
}
.diagram-container .mermaid {
border: none;
background: transparent;
padding: 0;
}
.diagram-controls {
display: flex;
align-items: center;
gap: 0.5rem;
}
.zoom-btn {
background: var(--bg-tertiary);
color: var(--text);
border: 1px solid var(--border);
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.25rem;
min-width: 32px;
height: 32px;
justify-content: center;
}
.zoom-btn:hover {
background: var(--primary);
color: white;
border-color: var(--primary);
}
.zoom-btn:active {
transform: scale(0.95);
}
.zoom-info {
font-size: 0.75rem;
color: var(--text-secondary);
margin-left: 0.5rem;
}
/* Responsive */
@@ -739,11 +803,18 @@
<div class="workflow-detail hidden" id="diagramSection">
<div class="section-header">
<h4>Workflow Diagram</h4>
<button id="copyDiagramBtn" class="copy-btn" title="Copy diagram code to clipboard">
📋 Copy
</button>
<div class="diagram-controls">
<button id="zoomInBtn" class="zoom-btn" title="Zoom In">🔍+</button>
<button id="zoomOutBtn" class="zoom-btn" title="Zoom Out">🔍-</button>
<button id="zoomResetBtn" class="zoom-btn" title="Reset Zoom">🔄</button>
<button id="copyDiagramBtn" class="copy-btn" title="Copy diagram code to clipboard">
📋 Copy
</button>
</div>
</div>
<div id="diagramContainer" class="diagram-container">
<div id="diagramViewer">Loading diagram...</div>
</div>
<div id="diagramViewer">Loading diagram...</div>
</div>
</div>
</div>
@@ -806,14 +877,23 @@
jsonViewer: document.getElementById('jsonViewer'),
diagramSection: document.getElementById('diagramSection'),
diagramViewer: document.getElementById('diagramViewer'),
diagramContainer: document.getElementById('diagramContainer'),
copyJsonBtn: document.getElementById('copyJsonBtn'),
copyDiagramBtn: document.getElementById('copyDiagramBtn')
copyDiagramBtn: document.getElementById('copyDiagramBtn'),
zoomInBtn: document.getElementById('zoomInBtn'),
zoomOutBtn: document.getElementById('zoomOutBtn'),
zoomResetBtn: document.getElementById('zoomResetBtn')
};
this.searchDebounceTimer = null;
this.currentWorkflow = null;
this.currentJsonData = null;
this.currentDiagramData = null;
this.diagramZoom = 1;
this.diagramSvg = null;
this.diagramPan = { x: 0, y: 0 };
this.isDragging = false;
this.lastMousePos = { x: 0, y: 0 };
this.init();
}
@@ -920,11 +1000,38 @@
this.copyToClipboard(this.currentDiagramData, 'copyDiagramBtn');
});
// Zoom control events
this.elements.zoomInBtn.addEventListener('click', () => {
this.zoomDiagram(1.2);
});
this.elements.zoomOutBtn.addEventListener('click', () => {
this.zoomDiagram(0.8);
});
this.elements.zoomResetBtn.addEventListener('click', () => {
this.resetDiagramZoom();
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this.closeModal();
}
// Zoom shortcuts when diagram is visible
if (!this.elements.diagramSection.classList.contains('hidden')) {
if (e.key === '+' || e.key === '=') {
e.preventDefault();
this.zoomDiagram(1.2);
} else if (e.key === '-') {
e.preventDefault();
this.zoomDiagram(0.8);
} else if (e.key === '0' && e.ctrlKey) {
e.preventDefault();
this.resetDiagramZoom();
}
}
});
}
@@ -1322,6 +1429,10 @@
this.currentWorkflow = null;
this.currentJsonData = null;
this.currentDiagramData = null;
this.diagramSvg = null;
this.diagramZoom = 1;
this.diagramPan = { x: 0, y: 0 };
this.isDragging = false;
// Reset button states
this.elements.viewJsonBtn.textContent = '📄 View JSON';
@@ -1382,6 +1493,13 @@
// Re-initialize Mermaid for the new diagram
if (typeof mermaid !== 'undefined') {
mermaid.init(undefined, this.elements.diagramViewer.querySelector('.mermaid'));
// Store reference to SVG and reset zoom
setTimeout(() => {
this.diagramSvg = this.elements.diagramViewer.querySelector('.mermaid svg');
this.resetDiagramZoom();
this.setupDiagramPanning();
}, 100);
}
} catch (error) {
this.elements.diagramViewer.textContent = 'Error loading diagram: ' + error.message;
@@ -1390,7 +1508,109 @@
}
}
updateLoadMoreButton() {
zoomDiagram(factor) {
if (!this.diagramSvg) return;
this.diagramZoom *= factor;
this.diagramZoom = Math.max(0.1, Math.min(10, this.diagramZoom)); // Limit zoom between 10% and 1000%
this.applyDiagramTransform();
}
resetDiagramZoom() {
this.diagramZoom = 1;
this.diagramPan = { x: 0, y: 0 };
this.applyDiagramTransform();
}
applyDiagramTransform() {
if (!this.diagramSvg) return;
const transform = `scale(${this.diagramZoom}) translate(${this.diagramPan.x}px, ${this.diagramPan.y}px)`;
this.diagramSvg.style.transform = transform;
this.diagramSvg.style.transformOrigin = 'center center';
}
setupDiagramPanning() {
if (!this.elements.diagramContainer) return;
// Mouse events
this.elements.diagramContainer.addEventListener('mousedown', (e) => {
if (e.button === 0) { // Left mouse button
this.startDragging(e.clientX, e.clientY);
e.preventDefault();
}
});
document.addEventListener('mousemove', (e) => {
if (this.isDragging) {
this.handleDragging(e.clientX, e.clientY);
e.preventDefault();
}
});
document.addEventListener('mouseup', () => {
this.stopDragging();
});
// Touch events for mobile
this.elements.diagramContainer.addEventListener('touchstart', (e) => {
if (e.touches.length === 1) {
const touch = e.touches[0];
this.startDragging(touch.clientX, touch.clientY);
e.preventDefault();
}
});
document.addEventListener('touchmove', (e) => {
if (this.isDragging && e.touches.length === 1) {
const touch = e.touches[0];
this.handleDragging(touch.clientX, touch.clientY);
e.preventDefault();
}
});
document.addEventListener('touchend', () => {
this.stopDragging();
});
// Prevent context menu on right click
this.elements.diagramContainer.addEventListener('contextmenu', (e) => {
e.preventDefault();
});
// Mouse wheel zoom
this.elements.diagramContainer.addEventListener('wheel', (e) => {
e.preventDefault();
const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
this.zoomDiagram(zoomFactor);
});
}
startDragging(x, y) {
this.isDragging = true;
this.lastMousePos = { x, y };
this.elements.diagramContainer.classList.add('dragging');
}
handleDragging(x, y) {
if (!this.isDragging) return;
const deltaX = x - this.lastMousePos.x;
const deltaY = y - this.lastMousePos.y;
// Apply pan delta scaled by zoom level (inverse relationship)
this.diagramPan.x += deltaX / this.diagramZoom;
this.diagramPan.y += deltaY / this.diagramZoom;
this.lastMousePos = { x, y };
this.applyDiagramTransform();
}
stopDragging() {
this.isDragging = false;
this.elements.diagramContainer.classList.remove('dragging');
} updateLoadMoreButton() {
const hasMore = this.state.currentPage < this.state.totalPages;
if (hasMore && this.state.workflows.length > 0) {

View File

@@ -529,12 +529,76 @@
border-radius: 0.5rem;
padding: 1rem;
text-align: center;
overflow-x: auto;
overflow: visible;
min-height: 300px;
}
.mermaid svg {
max-width: 100%;
max-width: none;
height: auto;
transition: transform 0.2s ease;
}
.diagram-container {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 0.5rem;
padding: 1rem;
text-align: center;
overflow: hidden;
height: 500px;
position: relative;
cursor: grab;
user-select: none;
}
.diagram-container.dragging {
cursor: grabbing;
}
.diagram-container .mermaid {
border: none;
background: transparent;
padding: 0;
}
.diagram-controls {
display: flex;
align-items: center;
gap: 0.5rem;
}
.zoom-btn {
background: var(--bg-tertiary);
color: var(--text);
border: 1px solid var(--border);
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.25rem;
min-width: 32px;
height: 32px;
justify-content: center;
}
.zoom-btn:hover {
background: var(--primary);
color: white;
border-color: var(--primary);
}
.zoom-btn:active {
transform: scale(0.95);
}
.zoom-info {
font-size: 0.75rem;
color: var(--text-secondary);
margin-left: 0.5rem;
}
/* Responsive */
@@ -739,11 +803,18 @@
<div class="workflow-detail hidden" id="diagramSection">
<div class="section-header">
<h4>Workflow Diagram</h4>
<button id="copyDiagramBtn" class="copy-btn" title="Copy diagram code to clipboard">
📋 Copy
</button>
<div class="diagram-controls">
<button id="zoomInBtn" class="zoom-btn" title="Zoom In">🔍+</button>
<button id="zoomOutBtn" class="zoom-btn" title="Zoom Out">🔍-</button>
<button id="zoomResetBtn" class="zoom-btn" title="Reset Zoom">🔄</button>
<button id="copyDiagramBtn" class="copy-btn" title="Copy diagram code to clipboard">
📋 Copy
</button>
</div>
</div>
<div id="diagramContainer" class="diagram-container">
<div id="diagramViewer">Loading diagram...</div>
</div>
<div id="diagramViewer">Loading diagram...</div>
</div>
</div>
</div>
@@ -806,14 +877,23 @@
jsonViewer: document.getElementById('jsonViewer'),
diagramSection: document.getElementById('diagramSection'),
diagramViewer: document.getElementById('diagramViewer'),
diagramContainer: document.getElementById('diagramContainer'),
copyJsonBtn: document.getElementById('copyJsonBtn'),
copyDiagramBtn: document.getElementById('copyDiagramBtn')
copyDiagramBtn: document.getElementById('copyDiagramBtn'),
zoomInBtn: document.getElementById('zoomInBtn'),
zoomOutBtn: document.getElementById('zoomOutBtn'),
zoomResetBtn: document.getElementById('zoomResetBtn')
};
this.searchDebounceTimer = null;
this.currentWorkflow = null;
this.currentJsonData = null;
this.currentDiagramData = null;
this.diagramZoom = 1;
this.diagramSvg = null;
this.diagramPan = { x: 0, y: 0 };
this.isDragging = false;
this.lastMousePos = { x: 0, y: 0 };
this.init();
}
@@ -920,11 +1000,38 @@
this.copyToClipboard(this.currentDiagramData, 'copyDiagramBtn');
});
// Zoom control events
this.elements.zoomInBtn.addEventListener('click', () => {
this.zoomDiagram(1.2);
});
this.elements.zoomOutBtn.addEventListener('click', () => {
this.zoomDiagram(0.8);
});
this.elements.zoomResetBtn.addEventListener('click', () => {
this.resetDiagramZoom();
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this.closeModal();
}
// Zoom shortcuts when diagram is visible
if (!this.elements.diagramSection.classList.contains('hidden')) {
if (e.key === '+' || e.key === '=') {
e.preventDefault();
this.zoomDiagram(1.2);
} else if (e.key === '-') {
e.preventDefault();
this.zoomDiagram(0.8);
} else if (e.key === '0' && e.ctrlKey) {
e.preventDefault();
this.resetDiagramZoom();
}
}
});
}
@@ -1322,6 +1429,10 @@
this.currentWorkflow = null;
this.currentJsonData = null;
this.currentDiagramData = null;
this.diagramSvg = null;
this.diagramZoom = 1;
this.diagramPan = { x: 0, y: 0 };
this.isDragging = false;
// Reset button states
this.elements.viewJsonBtn.textContent = '📄 View JSON';
@@ -1382,6 +1493,13 @@
// Re-initialize Mermaid for the new diagram
if (typeof mermaid !== 'undefined') {
mermaid.init(undefined, this.elements.diagramViewer.querySelector('.mermaid'));
// Store reference to SVG and reset zoom
setTimeout(() => {
this.diagramSvg = this.elements.diagramViewer.querySelector('.mermaid svg');
this.resetDiagramZoom();
this.setupDiagramPanning();
}, 100);
}
} catch (error) {
this.elements.diagramViewer.textContent = 'Error loading diagram: ' + error.message;
@@ -1390,7 +1508,109 @@
}
}
updateLoadMoreButton() {
zoomDiagram(factor) {
if (!this.diagramSvg) return;
this.diagramZoom *= factor;
this.diagramZoom = Math.max(0.1, Math.min(10, this.diagramZoom)); // Limit zoom between 10% and 1000%
this.applyDiagramTransform();
}
resetDiagramZoom() {
this.diagramZoom = 1;
this.diagramPan = { x: 0, y: 0 };
this.applyDiagramTransform();
}
applyDiagramTransform() {
if (!this.diagramSvg) return;
const transform = `scale(${this.diagramZoom}) translate(${this.diagramPan.x}px, ${this.diagramPan.y}px)`;
this.diagramSvg.style.transform = transform;
this.diagramSvg.style.transformOrigin = 'center center';
}
setupDiagramPanning() {
if (!this.elements.diagramContainer) return;
// Mouse events
this.elements.diagramContainer.addEventListener('mousedown', (e) => {
if (e.button === 0) { // Left mouse button
this.startDragging(e.clientX, e.clientY);
e.preventDefault();
}
});
document.addEventListener('mousemove', (e) => {
if (this.isDragging) {
this.handleDragging(e.clientX, e.clientY);
e.preventDefault();
}
});
document.addEventListener('mouseup', () => {
this.stopDragging();
});
// Touch events for mobile
this.elements.diagramContainer.addEventListener('touchstart', (e) => {
if (e.touches.length === 1) {
const touch = e.touches[0];
this.startDragging(touch.clientX, touch.clientY);
e.preventDefault();
}
});
document.addEventListener('touchmove', (e) => {
if (this.isDragging && e.touches.length === 1) {
const touch = e.touches[0];
this.handleDragging(touch.clientX, touch.clientY);
e.preventDefault();
}
});
document.addEventListener('touchend', () => {
this.stopDragging();
});
// Prevent context menu on right click
this.elements.diagramContainer.addEventListener('contextmenu', (e) => {
e.preventDefault();
});
// Mouse wheel zoom
this.elements.diagramContainer.addEventListener('wheel', (e) => {
e.preventDefault();
const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
this.zoomDiagram(zoomFactor);
});
}
startDragging(x, y) {
this.isDragging = true;
this.lastMousePos = { x, y };
this.elements.diagramContainer.classList.add('dragging');
}
handleDragging(x, y) {
if (!this.isDragging) return;
const deltaX = x - this.lastMousePos.x;
const deltaY = y - this.lastMousePos.y;
// Apply pan delta scaled by zoom level (inverse relationship)
this.diagramPan.x += deltaX / this.diagramZoom;
this.diagramPan.y += deltaY / this.diagramZoom;
this.lastMousePos = { x, y };
this.applyDiagramTransform();
}
stopDragging() {
this.isDragging = false;
this.elements.diagramContainer.classList.remove('dragging');
} updateLoadMoreButton() {
const hasMore = this.state.currentPage < this.state.totalPages;
if (hasMore && this.state.workflows.length > 0) {

View File

@@ -0,0 +1,286 @@
{
"name": "CalcsLive Demo Workflow Template",
"description": "Demonstrates @calcslive/n8n-nodes-calcslive custom node (https://www.npmjs.com/package/@calcslive/n8n-nodes-calcslive) that brings unit-aware physical quantities (PQ) and calculations to the n8n ecosystem in a composable manner. Example workflow with cylinder mass calculations.",
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-128,
-192
],
"id": "c6331ca9-2a74-419e-a15f-a11e5f3c0583",
"name": "When clicking 'Execute workflow'"
},
{
"parameters": {
"articleId": "3M6UW7CQB-2AP",
"inputPQs": {
"pq": [
{
"symbol": "D",
"value": 200,
"unit": "mm"
},
{
"symbol": "h",
"value": 20,
"unit": "cm"
}
]
},
"outputPQs": {
"pq": [
{
"symbol": "A",
"unit": "m^2"
},
{
"symbol": "V",
"unit": "m^3"
}
]
}
},
"type": "@calcslive/n8n-nodes-calcslive.calcsLive",
"typeVersion": 1,
"position": [
128,
-80
],
"id": "c22f212e-52ef-4d4f-b398-0bd4f2250705",
"name": "Cylinder Calcs: (D, h) => (A, V)",
"credentials": {
"calcsLiveApi": {
"id": "REPLACE_WITH_YOUR_CALCSLIVE_CREDENTIAL_ID",
"name": "Your CalcsLive API Credential"
}
}
},
{
"parameters": {
"articleId": "3M6UW7CQB-2AP",
"inputPQs": {
"pq": [
{
"symbol": "d",
"value": 360,
"unit": "km"
},
{
"symbol": "t",
"value": 10,
"unit": "h"
}
]
}
},
"type": "@calcslive/n8n-nodes-calcslive.calcsLive",
"typeVersion": 1,
"position": [
336,
-288
],
"id": "9b8cc0ea-d130-48a3-8552-4346f20a5ad0",
"name": "Speed Calc: (d, t) => v",
"credentials": {
"calcsLiveApi": {
"id": "REPLACE_WITH_YOUR_CALCSLIVE_CREDENTIAL_ID",
"name": "Your CalcsLive API Credential"
}
}
},
{
"parameters": {
"articleId": "3M6VLSBHB-3HT",
"inputPQs": {
"pq": [
{
"symbol": "ρ",
"value": 1000,
"unit": "kg/m^3"
},
{
"symbol": "V",
"value": "={{ $json.data.calculation.outputs.V.value }}",
"unit": "={{ $json.data.calculation.outputs.V.unit }}"
}
]
},
"outputPQs": {
"pq": [
{
"symbol": "m",
"unit": "kg"
}
]
}
},
"type": "@calcslive/n8n-nodes-calcslive.calcsLive",
"typeVersion": 1,
"position": [
336,
-80
],
"id": "5bf3e5ab-d1f6-42e7-9e64-b4b78bcbfa99",
"name": "Mass Calc: (ρ, V) => m",
"credentials": {
"calcsLiveApi": {
"id": "REPLACE_WITH_YOUR_CALCSLIVE_CREDENTIAL_ID",
"name": "Your CalcsLive API Credential"
}
}
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "a026dc84-665f-4898-8de9-ccdbaa530bfa",
"name": "Distance",
"value": 360,
"type": "number"
},
{
"id": "de7d6d3e-151c-4390-b2d9-bf78da0159eb",
"name": "DistanceUnit",
"value": "km",
"type": "string"
},
{
"id": "49c1e979-a0c2-403e-bed1-ef28eb8513ad",
"name": "Time",
"value": 2,
"type": "number"
},
{
"id": "54ae6f42-844f-4bf2-b6d3-a6e159d46e9e",
"name": "TimeUnit",
"value": "h",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
128,
-288
],
"id": "8b83b9e5-d7a4-4f74-b5bc-e6012a0f606a",
"name": "Fields: (d, t)"
},
{
"parameters": {
"sendTo": "user@example.com",
"subject": "CalcsLive Calculation Results",
"message": "=Hello!\n\nThis is an automated email from your n8n workflow using @calcslive/n8n-nodes-calcslive.\n\nCalculation Results:\n- Total Physical Quantities: {{ $json.data.calculation.totalPQs }}\n- Mass Result: {{ $json.data.calculation.outputs.m.value }} {{ $json.data.calculation.outputs.m.unit }}\n\nBest regards,\nYour CalcsLive Workflow",
"options": {
"attachmentsUi": {
"attachmentsBinary": []
}
}
},
"id": "2b48a81e-84f8-439d-aa58-6cc7b1c00480",
"name": "Send Email",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [
528,
-80
],
"credentials": {
"gmailOAuth2": {
"id": "REPLACE_WITH_YOUR_GMAIL_CREDENTIAL_ID",
"name": "Your Gmail Account"
}
}
}
],
"pinData": {},
"connections": {
"When clicking 'Execute workflow'": {
"main": [
[
{
"node": "Fields: (d, t)",
"type": "main",
"index": 0
},
{
"node": "Cylinder Calcs: (D, h) => (A, V)",
"type": "main",
"index": 0
}
]
]
},
"Speed Calc: (d, t) => v": {
"main": [
[]
]
},
"Mass Calc: (ρ, V) => m": {
"main": [
[
{
"node": "Send Email",
"type": "main",
"index": 0
}
]
]
},
"Fields: (d, t)": {
"main": [
[
{
"node": "Speed Calc: (d, t) => v",
"type": "main",
"index": 0
}
]
]
},
"Cylinder Calcs: (D, h) => (A, V)": {
"main": [
[
{
"node": "Mass Calc: (ρ, V) => m",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"meta": {
"templateCredsSetupCompleted": false
},
"tags": [
"calculation",
"calculator",
"math",
"engineering",
"unit-conversion",
"unit-converter",
"unit-aware",
"unit-awareness",
"physics",
"formula",
"computation",
"measurement",
"custom-node",
"PQ",
"physical-quantity",
"integration",
"composable"
]
}