@procaaso/alphinity-ui-components 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1351 @@
1
+ # P&ID Canvas Implementation Guide
2
+
3
+ ## Architecture Overview
4
+
5
+ This library provides **UI primitives** for building P&ID diagram editors and runtime viewers. Your application handles **state management** and **data binding**.
6
+
7
+ ```
8
+ ┌─────────────────────────────────────────────┐
9
+ │ Your Application (Engineering Mode) │
10
+ │ - Diagram state management │
11
+ │ - Save/load from database │
12
+ │ - Data stream configuration │
13
+ │ - User permissions │
14
+ └─────────────┬───────────────────────────────┘
15
+ │ Props & Callbacks
16
+
17
+ ┌─────────────────────────────────────────────┐
18
+ │ @procaaso/alphinity-ui-components │
19
+ │ - PIDCanvas (rendering & interaction) │
20
+ │ - Hooks (useTelemetry, useViewBox, etc.) │
21
+ │ - Visual components │
22
+ └─────────────────────────────────────────────┘
23
+ ```
24
+
25
+ ---
26
+
27
+ ## Quick Start: Minimal Implementation
28
+
29
+ ### 1. Basic Viewer (Read-Only)
30
+
31
+ ```tsx
32
+ import { PIDCanvas } from '@procaaso/alphinity-ui-components';
33
+ import type { PIDDiagram } from '@procaaso/alphinity-ui-components';
34
+
35
+ function DiagramViewer({ diagram }: { diagram: PIDDiagram }) {
36
+ return (
37
+ <div style={{ width: '100%', height: '100vh' }}>
38
+ <PIDCanvas
39
+ diagram={diagram}
40
+ features={{
41
+ pan: true,
42
+ zoom: true,
43
+ selection: false,
44
+ telemetry: false,
45
+ }}
46
+ />
47
+ </div>
48
+ );
49
+ }
50
+ ```
51
+
52
+ ### 2. Interactive Editor
53
+
54
+ ```tsx
55
+ import { PIDCanvas, SymbolLibrary } from '@procaaso/alphinity-ui-components';
56
+ import { useState } from 'react';
57
+ import type { PIDDiagram, NodeInstance, PipeEdge } from '@procaaso/alphinity-ui-components';
58
+
59
+ function DiagramEditor() {
60
+ const [diagram, setDiagram] = useState<PIDDiagram>({
61
+ nodes: [],
62
+ pipes: [],
63
+ viewBox: { x: 0, y: 0, width: 2000, height: 1500 },
64
+ });
65
+
66
+ const handleSymbolDrop = (symbolId: string, position: { x: number; y: number }) => {
67
+ const newNode: NodeInstance = {
68
+ id: `node-${Date.now()}`,
69
+ symbolId,
70
+ transform: { x: position.x, y: position.y, rotation: 0 },
71
+ label: symbolId,
72
+ };
73
+
74
+ setDiagram((prev) => ({
75
+ ...prev,
76
+ nodes: [...prev.nodes, newNode],
77
+ }));
78
+ };
79
+
80
+ const handlePipeCreated = (
81
+ sourceId: string,
82
+ targetId: string,
83
+ sourcePort?: string,
84
+ targetPort?: string
85
+ ) => {
86
+ const newPipe: PipeEdge = {
87
+ id: `pipe-${Date.now()}`,
88
+ fromNodeId: sourceId,
89
+ toNodeId: targetId,
90
+ fromPortId: sourcePort,
91
+ toPortId: targetPort,
92
+ routePoints: [], // Will be calculated by library
93
+ };
94
+
95
+ setDiagram((prev) => ({
96
+ ...prev,
97
+ pipes: [...prev.pipes, newPipe],
98
+ }));
99
+ };
100
+
101
+ const handleDelete = (nodeIds: string[], pipeIds: string[]) => {
102
+ setDiagram((prev) => ({
103
+ ...prev,
104
+ nodes: prev.nodes.filter((n) => !nodeIds.includes(n.id)),
105
+ pipes: prev.pipes.filter((p) => !pipeIds.includes(p.id)),
106
+ }));
107
+ };
108
+
109
+ return (
110
+ <div style={{ display: 'flex', height: '100vh' }}>
111
+ {/* Symbol Library */}
112
+ <SymbolLibrary onSymbolDragEnd={handleSymbolDrop} />
113
+
114
+ {/* Canvas */}
115
+ <div style={{ flex: 1 }}>
116
+ <PIDCanvas
117
+ diagram={diagram}
118
+ features={{
119
+ pan: true,
120
+ zoom: true,
121
+ selection: true,
122
+ dragNodes: true,
123
+ keyboardShortcuts: true,
124
+ connectionMode: true,
125
+ allowSymbolDrop: true,
126
+ }}
127
+ onSymbolDrop={handleSymbolDrop}
128
+ onPipeCreated={handlePipeCreated}
129
+ onDiagramChange={setDiagram}
130
+ onDelete={handleDelete}
131
+ />
132
+ </div>
133
+ </div>
134
+ );
135
+ }
136
+ ```
137
+
138
+ ---
139
+
140
+ ## Adding Custom SVG Symbols
141
+
142
+ The library comes with a basic set of P&ID symbols, but you can easily add your own. Symbols are static SVG definitions that can be reused across diagrams.
143
+
144
+ ### Symbol Structure
145
+
146
+ Each symbol is defined by the `SymbolDefinition` interface:
147
+
148
+ ```typescript
149
+ import type { SymbolDefinition } from '@procaaso/alphinity-ui-components';
150
+
151
+ const mySymbol: SymbolDefinition = {
152
+ id: 'tank-vertical', // Unique identifier
153
+ name: 'Vertical Tank', // Display name
154
+ category: 'vessel', // Category for grouping
155
+ viewBox: { x: 0, y: 0, width: 60, height: 100 }, // SVG coordinate space
156
+ ports: [
157
+ // Connection points for pipes
158
+ { id: 'inlet', x: 30, y: 0, type: 'inlet', direction: 'in' },
159
+ { id: 'outlet', x: 30, y: 100, type: 'outlet', direction: 'out' },
160
+ ],
161
+ svgContent: ` // SVG markup (inline as string)
162
+ <g>
163
+ <rect x="10" y="10" width="40" height="80"
164
+ fill="#b0bec5" stroke="#546e7a" stroke-width="2"/>
165
+ <line x1="30" y1="0" x2="30" y2="10"
166
+ stroke="#546e7a" stroke-width="3"/>
167
+ <line x1="30" y1="90" x2="30" y2="100"
168
+ stroke="#546e7a" stroke-width="3"/>
169
+ </g>
170
+ `,
171
+ metadata: {
172
+ // Optional metadata
173
+ description: 'Vertical storage tank',
174
+ tags: ['tank', 'vessel', 'storage'],
175
+ },
176
+ };
177
+ ```
178
+
179
+ ### Method 1: Extend the Built-in Library
180
+
181
+ Create a custom symbol library file in your app:
182
+
183
+ ```typescript
184
+ // src/symbols/customSymbols.ts
185
+ import type { SymbolDefinition } from '@procaaso/alphinity-ui-components';
186
+ import { mockSymbolLibrary } from '@procaaso/alphinity-ui-components';
187
+
188
+ export const customSymbols: Record<string, SymbolDefinition> = {
189
+ ...mockSymbolLibrary, // Include built-in symbols
190
+
191
+ 'tank-vertical': {
192
+ id: 'tank-vertical',
193
+ name: 'Vertical Tank',
194
+ category: 'vessel',
195
+ viewBox: { x: 0, y: 0, width: 60, height: 100 },
196
+ ports: [
197
+ { id: 'inlet', x: 30, y: 0, type: 'inlet', direction: 'in' },
198
+ { id: 'outlet', x: 30, y: 100, type: 'outlet', direction: 'out' },
199
+ { id: 'drain', x: 50, y: 100, type: 'outlet', direction: 'out' },
200
+ ],
201
+ svgContent: `
202
+ <g>
203
+ <!-- Tank body -->
204
+ <rect x="10" y="10" width="40" height="80"
205
+ fill="#b0bec5" stroke="#546e7a" stroke-width="2" rx="2"/>
206
+ <!-- Inlet pipe -->
207
+ <line x1="30" y1="0" x2="30" y2="10"
208
+ stroke="#546e7a" stroke-width="3" stroke-linecap="round"/>
209
+ <!-- Outlet pipe -->
210
+ <line x1="30" y1="90" x2="30" y2="100"
211
+ stroke="#546e7a" stroke-width="3" stroke-linecap="round"/>
212
+ <!-- Drain pipe -->
213
+ <line x1="50" y1="90" x2="50" y2="100"
214
+ stroke="#546e7a" stroke-width="2" stroke-linecap="round"/>
215
+ <!-- Level indicator -->
216
+ <line x1="12" y1="50" x2="48" y2="50"
217
+ stroke="#0277bd" stroke-width="1" stroke-dasharray="2,2"/>
218
+ </g>
219
+ `,
220
+ metadata: {
221
+ description: 'Vertical storage tank with drain',
222
+ tags: ['tank', 'vessel', 'storage'],
223
+ },
224
+ },
225
+
226
+ 'motor-electric': {
227
+ id: 'motor-electric',
228
+ name: 'Electric Motor',
229
+ category: 'actuator',
230
+ viewBox: { x: 0, y: 0, width: 50, height: 50 },
231
+ ports: [{ id: 'shaft', x: 25, y: 0, type: 'outlet', direction: 'out' }],
232
+ svgContent: `
233
+ <g>
234
+ <!-- Motor housing -->
235
+ <circle cx="25" cy="25" r="20"
236
+ fill="#546e7a" stroke="#263238" stroke-width="2"/>
237
+ <!-- M label -->
238
+ <text x="25" y="32" text-anchor="middle"
239
+ font-size="20" font-weight="bold" fill="#ffffff">M</text>
240
+ <!-- Shaft -->
241
+ <line x1="25" y1="0" x2="25" y2="5"
242
+ stroke="#263238" stroke-width="3" stroke-linecap="round"/>
243
+ </g>
244
+ `,
245
+ },
246
+ };
247
+
248
+ // Helper to get symbols
249
+ export function getCustomSymbol(id: string): SymbolDefinition | undefined {
250
+ return customSymbols[id];
251
+ }
252
+ ```
253
+
254
+ Then use your custom library in components:
255
+
256
+ ```tsx
257
+ import { SymbolLibrary } from '@procaaso/alphinity-ui-components';
258
+ import { customSymbols } from './symbols/customSymbols';
259
+
260
+ function App() {
261
+ return (
262
+ <SymbolLibrary symbols={Object.values(customSymbols)} onSymbolDragEnd={handleSymbolDrop} />
263
+ );
264
+ }
265
+ ```
266
+
267
+ ### Method 2: Import from SVG Files
268
+
269
+ If you have existing SVG files, extract the content:
270
+
271
+ ```typescript
272
+ // symbols/fromSvgFile.ts
273
+ export const valveFromFile: SymbolDefinition = {
274
+ id: 'valve-butterfly',
275
+ name: 'Butterfly Valve',
276
+ category: 'valve',
277
+ viewBox: { x: 0, y: 0, width: 60, height: 40 },
278
+ ports: [
279
+ { id: 'inlet', x: 0, y: 20, type: 'inlet', direction: 'in' },
280
+ { id: 'outlet', x: 60, y: 20, type: 'outlet', direction: 'out' },
281
+ ],
282
+ // Copy/paste the SVG content from your .svg file
283
+ // Remove the <svg> wrapper and any <?xml> declarations
284
+ svgContent: `
285
+ <g>
286
+ <!-- Paste SVG content here -->
287
+ <path d="M 10 10 L 50 30 L 10 50 Z" fill="#ccc" stroke="#333" stroke-width="2"/>
288
+ </g>
289
+ `,
290
+ };
291
+ ```
292
+
293
+ **From diagrams.net/draw.io exports:**
294
+
295
+ 1. Export as SVG
296
+ 2. Open in text editor
297
+ 3. Copy everything between `<svg>...</svg>`
298
+ 4. Remove `<svg>` tags, keep inner `<g>` content
299
+ 5. Paste into `svgContent`
300
+
301
+ ### Method 3: Database-Backed Symbols
302
+
303
+ For enterprise apps, store symbols in a database:
304
+
305
+ ```typescript
306
+ // api/symbols.ts
307
+ export async function loadSymbolLibrary(): Promise<Record<string, SymbolDefinition>> {
308
+ const response = await fetch('/api/symbols');
309
+ const symbols = await response.json();
310
+
311
+ return symbols.reduce((acc, symbol) => {
312
+ acc[symbol.id] = symbol;
313
+ return acc;
314
+ }, {});
315
+ }
316
+
317
+ // Usage in app
318
+ function App() {
319
+ const [symbols, setSymbols] = useState<Record<string, SymbolDefinition>>({});
320
+
321
+ useEffect(() => {
322
+ loadSymbolLibrary().then(setSymbols);
323
+ }, []);
324
+
325
+ return <SymbolLibrary symbols={Object.values(symbols)} />;
326
+ }
327
+ ```
328
+
329
+ ### Port Positioning Rules
330
+
331
+ Ports define where pipes connect to symbols:
332
+
333
+ ```typescript
334
+ ports: [
335
+ {
336
+ id: 'inlet', // Unique within this symbol
337
+ x: 0, // X coordinate in viewBox space
338
+ y: 25, // Y coordinate in viewBox space
339
+ type: 'inlet', // Type (inlet, outlet, or custom)
340
+ direction: 'in', // Optional: flow direction hint
341
+ },
342
+ ];
343
+ ```
344
+
345
+ **Key points:**
346
+
347
+ - Coordinates are relative to `viewBox` origin (top-left)
348
+ - Ports at edges should be at `x: 0`, `x: width`, `y: 0`, or `y: height`
349
+ - Multiple ports are supported (manifolds, 3-way valves, etc.)
350
+ - Port types are informational only - library doesn't enforce flow direction
351
+
352
+ **Example multi-port symbol:**
353
+
354
+ ```typescript
355
+ {
356
+ id: 'valve-3way',
357
+ viewBox: { x: 0, y: 0, width: 60, height: 60 },
358
+ ports: [
359
+ { id: 'inlet', x: 0, y: 30, type: 'inlet' },
360
+ { id: 'outlet-1', x: 60, y: 20, type: 'outlet' },
361
+ { id: 'outlet-2', x: 60, y: 40, type: 'outlet' },
362
+ ],
363
+ // SVG content...
364
+ }
365
+ ```
366
+
367
+ ### SVG Styling Best Practices
368
+
369
+ **Use inline styles for portability:**
370
+
371
+ ```svg
372
+ <!-- ✅ GOOD - Inline attributes -->
373
+ <rect fill="#546e7a" stroke="#263238" stroke-width="2"/>
374
+
375
+ <!-- ❌ BAD - CSS classes (won't work) -->
376
+ <rect class="tank-body"/>
377
+ ```
378
+
379
+ **Keep colors consistent:**
380
+
381
+ ```typescript
382
+ // Standard industrial colors
383
+ const COLORS = {
384
+ steel: '#546e7a', // Equipment bodies
385
+ pipe: '#263238', // Pipes and connections
386
+ fluid: '#0277bd', // Liquid indicators
387
+ actuator: '#f57c00', // Actuators/motors
388
+ alarm: '#d32f2f', // Alarms/warnings
389
+ };
390
+ ```
391
+
392
+ **Optimize for zoom:**
393
+
394
+ - Use `stroke-width: 2` or higher (visible when zoomed out)
395
+ - Avoid text unless it's large (16px+)
396
+ - Use simple shapes (better performance)
397
+
398
+ ### Testing New Symbols
399
+
400
+ Add to the demo to preview:
401
+
402
+ ```tsx
403
+ // demo/RuntimeDemo.tsx
404
+ import { customSymbols } from '../src/symbols/customSymbols';
405
+
406
+ // Test your symbol
407
+ const testDiagram = {
408
+ nodes: [
409
+ {
410
+ id: 'test-1',
411
+ symbolId: 'tank-vertical', // Your custom symbol
412
+ transform: { x: 100, y: 100, rotation: 0 },
413
+ },
414
+ ],
415
+ pipes: [],
416
+ viewBox: { x: 0, y: 0, width: 2000, height: 1500 },
417
+ };
418
+ ```
419
+
420
+ ---
421
+
422
+ ## Production Implementation Pattern
423
+
424
+ ### Application Structure
425
+
426
+ ```
427
+ your-app/
428
+ ├── src/
429
+ │ ├── features/
430
+ │ │ ├── engineering-mode/ # Edit mode
431
+ │ │ │ ├── DiagramEditor.tsx
432
+ │ │ │ ├── DataStreamConfig.tsx
433
+ │ │ │ └── useEngineeringMode.ts
434
+ │ │ ├── runtime-mode/ # HMI viewer
435
+ │ │ │ ├── RuntimeViewer.tsx
436
+ │ │ │ ├── TelemetryBinding.tsx
437
+ │ │ │ └── useDataStreams.ts
438
+ │ │ └── persistence/
439
+ │ │ ├── useDiagramStorage.ts
440
+ │ │ └── api.ts
441
+ │ └── components/
442
+ │ └── DiagramWorkspace.tsx # Mode switcher
443
+ ```
444
+
445
+ ### State Management Hook
446
+
447
+ ```tsx
448
+ // features/persistence/useDiagramStorage.ts
449
+ import { useState, useEffect } from 'react';
450
+ import type { PIDDiagram } from '@procaaso/alphinity-ui-components';
451
+
452
+ export function useDiagramStorage(diagramId: string) {
453
+ const [diagram, setDiagram] = useState<PIDDiagram | null>(null);
454
+ const [loading, setLoading] = useState(true);
455
+ const [saving, setSaving] = useState(false);
456
+
457
+ // Load from database
458
+ useEffect(() => {
459
+ async function load() {
460
+ const response = await fetch(`/api/diagrams/${diagramId}`);
461
+ const data = await response.json();
462
+ setDiagram(data);
463
+ setLoading(false);
464
+ }
465
+ load();
466
+ }, [diagramId]);
467
+
468
+ // Save to database (debounced)
469
+ const saveDiagram = async (updated: PIDDiagram) => {
470
+ setDiagram(updated);
471
+ setSaving(true);
472
+
473
+ try {
474
+ await fetch(`/api/diagrams/${diagramId}`, {
475
+ method: 'PUT',
476
+ headers: { 'Content-Type': 'application/json' },
477
+ body: JSON.stringify(updated),
478
+ });
479
+ } finally {
480
+ setSaving(false);
481
+ }
482
+ };
483
+
484
+ return { diagram, loading, saving, saveDiagram };
485
+ }
486
+ ```
487
+
488
+ ### Engineering Mode (Edit)
489
+
490
+ ```tsx
491
+ // features/engineering-mode/DiagramEditor.tsx
492
+ import { PIDCanvas, SymbolLibrary } from '@procaaso/alphinity-ui-components';
493
+ import { useDiagramStorage } from '../persistence/useDiagramStorage';
494
+ import { DataStreamConfig } from './DataStreamConfig';
495
+
496
+ export function DiagramEditor({ diagramId }: { diagramId: string }) {
497
+ const { diagram, loading, saveDiagram } = useDiagramStorage(diagramId);
498
+ const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
499
+
500
+ if (loading || !diagram) return <div>Loading...</div>;
501
+
502
+ return (
503
+ <div style={{ display: 'flex', height: '100vh' }}>
504
+ {/* Left: Symbol Library */}
505
+ <div style={{ width: 250, borderRight: '1px solid #ccc' }}>
506
+ <SymbolLibrary
507
+ onSymbolDragEnd={(symbolId, x, y) => {
508
+ // Add node to diagram
509
+ const newNode = {
510
+ id: `node-${Date.now()}`,
511
+ symbolId,
512
+ transform: { x, y, rotation: 0 },
513
+ };
514
+ saveDiagram({
515
+ ...diagram,
516
+ nodes: [...diagram.nodes, newNode],
517
+ });
518
+ }}
519
+ />
520
+ </div>
521
+
522
+ {/* Center: Canvas */}
523
+ <div style={{ flex: 1 }}>
524
+ <PIDCanvas
525
+ diagram={diagram}
526
+ features={{
527
+ selection: true,
528
+ dragNodes: true,
529
+ connectionMode: true,
530
+ keyboardShortcuts: true,
531
+ allowSymbolDrop: true,
532
+ }}
533
+ onNodeClick={setSelectedNodeId}
534
+ onDiagramChange={saveDiagram}
535
+ onDelete={(nodeIds, pipeIds) => {
536
+ saveDiagram({
537
+ ...diagram,
538
+ nodes: diagram.nodes.filter((n) => !nodeIds.includes(n.id)),
539
+ pipes: diagram.pipes.filter((p) => !pipeIds.includes(p.id)),
540
+ });
541
+ }}
542
+ />
543
+ </div>
544
+
545
+ {/* Right: Configuration Panel */}
546
+ <div style={{ width: 300, borderLeft: '1px solid #ccc' }}>
547
+ {selectedNodeId && (
548
+ <DataStreamConfig nodeId={selectedNodeId} diagram={diagram} onUpdate={saveDiagram} />
549
+ )}
550
+ </div>
551
+ </div>
552
+ );
553
+ }
554
+ ```
555
+
556
+ ### Data Stream Configuration
557
+
558
+ ```tsx
559
+ // features/engineering-mode/DataStreamConfig.tsx
560
+ import { useState } from 'react';
561
+ import type { PIDDiagram } from '@procaaso/alphinity-ui-components';
562
+
563
+ export function DataStreamConfig({
564
+ nodeId,
565
+ diagram,
566
+ onUpdate,
567
+ }: {
568
+ nodeId: string;
569
+ diagram: PIDDiagram;
570
+ onUpdate: (diagram: PIDDiagram) => void;
571
+ }) {
572
+ const node = diagram.nodes.find((n) => n.id === nodeId);
573
+ const [dataSource, setDataSource] = useState('');
574
+
575
+ const handleBindData = () => {
576
+ // Store data binding metadata on the node
577
+ const updatedNodes = diagram.nodes.map((n) =>
578
+ n.id === nodeId
579
+ ? {
580
+ ...n,
581
+ metadata: {
582
+ ...n.metadata,
583
+ dataSource,
584
+ dataType: 'temperature',
585
+ unit: '°F',
586
+ },
587
+ }
588
+ : n
589
+ );
590
+
591
+ onUpdate({ ...diagram, nodes: updatedNodes });
592
+ };
593
+
594
+ return (
595
+ <div style={{ padding: 16 }}>
596
+ <h3>Data Stream Configuration</h3>
597
+ <p>Node: {node?.label || nodeId}</p>
598
+
599
+ <label>
600
+ Data Source (Tag ID):
601
+ <input
602
+ type="text"
603
+ value={dataSource}
604
+ onChange={(e) => setDataSource(e.target.value)}
605
+ placeholder="e.g., PLC1.Tank01.Temp"
606
+ />
607
+ </label>
608
+
609
+ <button onClick={handleBindData}>Bind Data Stream</button>
610
+
611
+ <div style={{ marginTop: 20 }}>
612
+ <h4>Current Bindings:</h4>
613
+ <pre>{JSON.stringify(node?.metadata, null, 2)}</pre>
614
+ </div>
615
+ </div>
616
+ );
617
+ }
618
+ ```
619
+
620
+ ### Runtime Mode (HMI Viewer)
621
+
622
+ ```tsx
623
+ // features/runtime-mode/RuntimeViewer.tsx
624
+ import { PIDCanvas } from '@procaaso/alphinity-ui-components';
625
+ import { useDiagramStorage } from '../persistence/useDiagramStorage';
626
+ import { useDataStreams } from './useDataStreams';
627
+
628
+ export function RuntimeViewer({ diagramId }: { diagramId: string }) {
629
+ const { diagram, loading } = useDiagramStorage(diagramId);
630
+
631
+ // Connect to real-time data (WebSocket, MQTT, OPC UA, etc.)
632
+ const telemetry = useDataStreams(diagram?.nodes || []);
633
+
634
+ if (loading || !diagram) return <div>Loading...</div>;
635
+
636
+ return (
637
+ <div style={{ width: '100%', height: '100vh' }}>
638
+ <PIDCanvas
639
+ diagram={diagram}
640
+ features={{
641
+ pan: true,
642
+ zoom: true,
643
+ telemetry: true,
644
+ }}
645
+ telemetry={telemetry}
646
+ />
647
+ </div>
648
+ );
649
+ }
650
+ ```
651
+
652
+ ### Real-Time Data Hook
653
+
654
+ ```tsx
655
+ // features/runtime-mode/useDataStreams.ts
656
+ import { useState, useEffect } from 'react';
657
+ import type {
658
+ NodeInstance,
659
+ TelemetryMap,
660
+ TelemetryBinding,
661
+ } from '@procaaso/alphinity-ui-components';
662
+
663
+ export function useDataStreams(nodes: NodeInstance[]): TelemetryMap {
664
+ const [telemetry, setTelemetry] = useState<TelemetryMap>(new Map());
665
+
666
+ useEffect(() => {
667
+ // Example: Connect to WebSocket
668
+ const ws = new WebSocket('wss://your-scada-server/data');
669
+
670
+ ws.onmessage = (event) => {
671
+ const update = JSON.parse(event.data);
672
+
673
+ // Match incoming data to nodes
674
+ const updatedTelemetry = new Map(telemetry);
675
+
676
+ nodes.forEach((node) => {
677
+ const dataSource = node.metadata?.dataSource;
678
+ if (dataSource && update[dataSource]) {
679
+ const binding: TelemetryBinding = {
680
+ nodeId: node.id,
681
+ tagId: dataSource,
682
+ value: update[dataSource].value,
683
+ timestamp: new Date(update[dataSource].timestamp),
684
+ quality: update[dataSource].quality || 'good',
685
+ unit: node.metadata?.unit || '',
686
+ display: {
687
+ label: node.label || dataSource,
688
+ valueColor: '#ffffff',
689
+ },
690
+ };
691
+ updatedTelemetry.set(node.id, binding);
692
+ }
693
+ });
694
+
695
+ setTelemetry(updatedTelemetry);
696
+ };
697
+
698
+ return () => ws.close();
699
+ }, [nodes]);
700
+
701
+ return telemetry;
702
+ }
703
+ ```
704
+
705
+ ---
706
+
707
+ ## Layout & Sizing
708
+
709
+ ### Container Requirements
710
+
711
+ **PIDCanvas requires a sized container**. It uses `width: 100%` and `height: 100%` internally, so the parent must have explicit dimensions:
712
+
713
+ ```tsx
714
+ // ✅ GOOD - Parent has explicit height
715
+ <div style={{ width: '100%', height: '100vh' }}>
716
+ <PIDCanvas diagram={diagram} />
717
+ </div>
718
+
719
+ // ✅ GOOD - Flexbox with flex: 1
720
+ <div style={{ display: 'flex', height: '100vh' }}>
721
+ <Sidebar />
722
+ <div style={{ flex: 1 }}>
723
+ <PIDCanvas diagram={diagram} />
724
+ </div>
725
+ </div>
726
+
727
+ // ❌ BAD - Parent has no height
728
+ <div>
729
+ <PIDCanvas diagram={diagram} /> {/* Will collapse to 0 height */}
730
+ </div>
731
+ ```
732
+
733
+ ### ViewBox Coordinates
734
+
735
+ The `viewBox` defines the world coordinate system (in SVG units), independent of pixel dimensions:
736
+
737
+ ```tsx
738
+ diagram.viewBox = { x: 0, y: 0, width: 2000, height: 1500 };
739
+ // Node at { x: 1000, y: 750 } is at the center
740
+ // Works on any screen size (mobile, desktop, ultrawide)
741
+ ```
742
+
743
+ **Key Points:**
744
+
745
+ - Node positions (transform.x/y) are in viewBox units, not pixels
746
+ - Pan/zoom modifies the viewBox, not node positions
747
+ - Grid spacing (25 units) scales with zoom level
748
+ - Use consistent viewBox dimensions across diagrams for predictable spacing
749
+
750
+ ### Aspect Ratio Preservation
751
+
752
+ PIDCanvas uses `preserveAspectRatio="xMidYMid meet"`:
753
+
754
+ - Content is centered and scaled to fit
755
+ - Maintains aspect ratio (no distortion)
756
+ - May add letterboxing (top/bottom) or pillarboxing (sides) on ultra-wide/tall displays
757
+
758
+ This is handled automatically - coordinate transformation accounts for it.
759
+
760
+ ### Performance Considerations
761
+
762
+ **Large Diagrams (100+ nodes):**
763
+
764
+ - Each node/pipe is a separate SVG element (good for interactivity)
765
+ - Port indicators render for all nodes in connection mode
766
+ - Consider implementing viewport culling if needed (future enhancement)
767
+
768
+ **High-Frequency Telemetry Updates:**
769
+
770
+ - Use `useTelemetry` hook with batched updates
771
+ - Debounce incoming data streams (max 1-2 updates/sec per node)
772
+ - DataOverlay components re-render only when their telemetry changes
773
+
774
+ ---
775
+
776
+ ## Recommended Patterns (From Demo)
777
+
778
+ These patterns from `RuntimeDemo.tsx` represent best practices for production implementations:
779
+
780
+ ### 1. Edit/Runtime Mode Switching
781
+
782
+ ```tsx
783
+ function DiagramWorkspace({ diagramId }: { diagramId: string }) {
784
+ const [mode, setMode] = useState<'edit' | 'runtime'>('edit');
785
+ const { diagram, saveDiagram } = useDiagramStorage(diagramId);
786
+
787
+ return (
788
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
789
+ {/* Mode Toggle */}
790
+ <header style={{ padding: 10, borderBottom: '1px solid #ccc' }}>
791
+ <button onClick={() => setMode('edit')}>Edit Mode</button>
792
+ <button onClick={() => setMode('runtime')}>Runtime Mode</button>
793
+ </header>
794
+
795
+ {/* Canvas with mode-specific features */}
796
+ <div style={{ flex: 1, display: 'flex' }}>
797
+ {mode === 'edit' && <SymbolLibrary />}
798
+
799
+ <div style={{ flex: 1 }}>
800
+ <PIDCanvas
801
+ diagram={diagram}
802
+ features={{
803
+ // Edit features
804
+ selection: mode === 'edit',
805
+ dragNodes: mode === 'edit',
806
+ connectionMode: mode === 'edit',
807
+ keyboardShortcuts: mode === 'edit',
808
+ allowSymbolDrop: mode === 'edit',
809
+ editPipeRoutes: mode === 'edit',
810
+
811
+ // Runtime features
812
+ telemetry: mode === 'runtime',
813
+
814
+ // Always enabled
815
+ pan: true,
816
+ zoom: true,
817
+ }}
818
+ onDiagramChange={mode === 'edit' ? saveDiagram : undefined}
819
+ telemetry={mode === 'runtime' ? telemetry : undefined}
820
+ />
821
+ </div>
822
+ </div>
823
+ </div>
824
+ );
825
+ }
826
+ ```
827
+
828
+ **Why this pattern:**
829
+
830
+ - Clear separation of concerns (editing vs monitoring)
831
+ - Prevents accidental edits in production HMI
832
+ - Features are explicitly tied to user role/mode
833
+
834
+ ### 2. Connection Mode with Visual Port Selection
835
+
836
+ ```tsx
837
+ const [connectionMode, setConnectionMode] = useState(false);
838
+
839
+ <PIDCanvas
840
+ features={{
841
+ connectionMode: connectionMode && mode === 'edit',
842
+ // Disable other interactions during connection
843
+ selection: !connectionMode && mode === 'edit',
844
+ dragNodes: !connectionMode && mode === 'edit',
845
+ }}
846
+ onPipeCreated={(sourceId, targetId, sourcePort, targetPort) => {
847
+ const newPipe = {
848
+ id: `pipe-${Date.now()}`,
849
+ fromNodeId: sourceId,
850
+ toNodeId: targetId,
851
+ fromPortId: sourcePort, // User-selected port
852
+ toPortId: targetPort, // User-selected port
853
+ routePoints: [],
854
+ };
855
+ saveDiagram({ ...diagram, pipes: [...diagram.pipes, newPipe] });
856
+ setConnectionMode(false);
857
+ }}
858
+ />;
859
+
860
+ {
861
+ /* Connection Mode Toggle */
862
+ }
863
+ <button onClick={() => setConnectionMode(!connectionMode)}>
864
+ {connectionMode ? 'Cancel Connection' : 'Connect Nodes'}
865
+ </button>;
866
+ ```
867
+
868
+ **Why this pattern:**
869
+
870
+ - User has full control over which ports connect
871
+ - Visual feedback (port circles, rubber-band line)
872
+ - No directional enforcement (any port → any port)
873
+ - Works with node rotation (ports move with transforms)
874
+
875
+ ### 3. Auto-Save with Diagram Changes
876
+
877
+ ```tsx
878
+ const debouncedSave = useDebouncedCallback(
879
+ (diagram: PIDDiagram) => {
880
+ fetch(`/api/diagrams/${diagramId}`, {
881
+ method: 'PUT',
882
+ body: JSON.stringify(diagram),
883
+ });
884
+ },
885
+ 1000 // Save 1 second after last change
886
+ );
887
+
888
+ <PIDCanvas
889
+ onDiagramChange={debouncedSave}
890
+ onNodeMove={debouncedSave}
891
+ // Other callbacks also trigger auto-save
892
+ />;
893
+ ```
894
+
895
+ **Why this pattern:**
896
+
897
+ - No manual "Save" button needed
898
+ - Prevents data loss from browser crashes
899
+ - Debouncing prevents excessive API calls during drag operations
900
+
901
+ ### 4. Three-Panel Layout (Symbol Library + Canvas + Config)
902
+
903
+ ```tsx
904
+ <div style={{ display: 'flex', height: '100vh' }}>
905
+ {/* Left: Symbol Library */}
906
+ <div style={{ width: 250, borderRight: '1px solid #ccc', overflowY: 'auto' }}>
907
+ <SymbolLibrary onSymbolDragEnd={handleSymbolDrop} />
908
+ </div>
909
+
910
+ {/* Center: Canvas */}
911
+ <div style={{ flex: 1, position: 'relative' }}>
912
+ <PIDCanvas
913
+ diagram={diagram}
914
+ onNodeClick={setSelectedNodeId}
915
+ // ... other props
916
+ />
917
+
918
+ {/* Connection mode toggle overlay */}
919
+ {mode === 'edit' && (
920
+ <button
921
+ style={{ position: 'absolute', top: 20, left: 20 }}
922
+ onClick={() => setConnectionMode(!connectionMode)}
923
+ >
924
+ {connectionMode ? '✖ Cancel' : '🔗 Connect'}
925
+ </button>
926
+ )}
927
+ </div>
928
+
929
+ {/* Right: Configuration Panel */}
930
+ {selectedNodeId && (
931
+ <div style={{ width: 300, borderLeft: '1px solid #ccc', padding: 16 }}>
932
+ <NodeConfigPanel nodeId={selectedNodeId} diagram={diagram} onUpdate={saveDiagram} />
933
+ </div>
934
+ )}
935
+ </div>
936
+ ```
937
+
938
+ **Why this pattern:**
939
+
940
+ - Familiar CAD/drawing tool UX
941
+ - Symbol library always accessible
942
+ - Configuration panel appears when node selected
943
+ - Canvas takes remaining space (responsive)
944
+
945
+ ### 5. Keyboard Shortcuts
946
+
947
+ ```tsx
948
+ <PIDCanvas
949
+ features={{
950
+ keyboardShortcuts: mode === 'edit',
951
+ }}
952
+ onDelete={(nodeIds, pipeIds) => {
953
+ const updatedDiagram = {
954
+ ...diagram,
955
+ nodes: diagram.nodes.filter((n) => !nodeIds.includes(n.id)),
956
+ pipes: diagram.pipes.filter((p) => !pipeIds.includes(p.id)),
957
+ };
958
+ saveDiagram(updatedDiagram);
959
+ }}
960
+ />
961
+ ```
962
+
963
+ **Built-in shortcuts when enabled:**
964
+
965
+ - `Delete` - Remove selected nodes/pipes
966
+ - `Ctrl+C` - Copy selection (calls `onItemsCopied`)
967
+ - `Ctrl+V` - Paste (calls `onPaste`)
968
+
969
+ **Why this pattern:**
970
+
971
+ - Industry-standard shortcuts
972
+ - Opt-in (disabled in runtime mode)
973
+ - You implement the actual copy/paste logic
974
+
975
+ ### 6. Port-Based Routing (No Direction Enforcement)
976
+
977
+ ```tsx
978
+ // Library auto-calculates route between any two ports
979
+ onPipeCreated={(sourceId, targetId, sourcePort, targetPort) => {
980
+ // sourcePort and targetPort can be any port (inlet, outlet, or custom)
981
+ // Library doesn't enforce "outlet → inlet" direction
982
+ const pipe = {
983
+ fromNodeId: sourceId,
984
+ toNodeId: targetId,
985
+ fromPortId: sourcePort || 'outlet', // Fallback to default if not specified
986
+ toPortId: targetPort || 'inlet',
987
+ routePoints: [], // Library calculates orthogonal routing
988
+ };
989
+ // ...
990
+ }
991
+ ```
992
+
993
+ **Why this pattern:**
994
+
995
+ - Supports non-standard connections (recirculation loops, bypass lines)
996
+ - Works with rotated nodes (ports maintain correct positions)
997
+ - Users can rotate nodes after connecting without breaking pipes
998
+ - Future-proof for multi-port symbols (3-way valves, manifolds)
999
+
1000
+ ---
1001
+
1002
+ ## Best Practices
1003
+
1004
+ ### 1. Separate Edit and Runtime Modes
1005
+
1006
+ **Don't** enable interactive features in runtime mode:
1007
+
1008
+ ```tsx
1009
+ // ❌ BAD - allows editing in production HMI
1010
+ <PIDCanvas features={{ selection: true, dragNodes: true }} />
1011
+ ```
1012
+
1013
+ **Do** use role-based feature flags:
1014
+
1015
+ ```tsx
1016
+ // ✅ GOOD - features based on user role
1017
+ <PIDCanvas
1018
+ features={{
1019
+ selection: user.role === 'engineer',
1020
+ dragNodes: user.role === 'engineer',
1021
+ telemetry: mode === 'runtime',
1022
+ }}
1023
+ />
1024
+ ```
1025
+
1026
+ ### 2. Store Data Bindings as Metadata
1027
+
1028
+ ```tsx
1029
+ // Store on node.metadata (not in telemetry)
1030
+ const node: NodeInstance = {
1031
+ id: 'tank-01',
1032
+ symbolId: 'tank-vertical',
1033
+ transform: { x: 100, y: 100, rotation: 0 },
1034
+ metadata: {
1035
+ // Your application data
1036
+ dataSource: 'PLC1.TK001.Level',
1037
+ unit: '%',
1038
+ alarmHigh: 95,
1039
+ alarmLow: 10,
1040
+ },
1041
+ };
1042
+ ```
1043
+
1044
+ ### 3. Lazy Load Diagrams
1045
+
1046
+ ```tsx
1047
+ // Don't load all diagrams at once
1048
+ const diagrams = await fetch('/api/diagrams?summary=true');
1049
+
1050
+ // Load full diagram when needed
1051
+ const fullDiagram = await fetch(`/api/diagrams/${id}`);
1052
+ ```
1053
+
1054
+ ### 4. Debounce Saves
1055
+
1056
+ ````tsx
1057
+ import { useDebouncedCallback } from 'use-debounce';
1058
+
1059
+ const debouncedSave = useDebouncedCallback((diagram) => {
1060
+ saveDiagram(diagram);
1061
+ ### 4. Debounce Saves
1062
+
1063
+ ```tsx
1064
+ import { useDebouncedCallback } from 'use-debounce';
1065
+
1066
+ const debouncedSave = useDebouncedCallback((diagram) => {
1067
+ saveDiagram(diagram);
1068
+ }, 1000);
1069
+ ````
1070
+
1071
+ ### 5. Handle Connection Mode State
1072
+
1073
+ When connection mode is active, disable conflicting features:
1074
+
1075
+ ```tsx
1076
+ const features = {
1077
+ selection: mode === 'edit' && !connectionMode,
1078
+ dragNodes: mode === 'edit' && !connectionMode,
1079
+ connectionMode: mode === 'edit' && connectionMode,
1080
+ editPipeRoutes: mode === 'edit' && !connectionMode,
1081
+ };
1082
+ ```
1083
+
1084
+ **Why:** Prevents confusing UX where user tries to drag nodes while connecting pipes.
1085
+
1086
+ ### 6. Use Absolute Positioning for Overlays
1087
+
1088
+ ```tsx
1089
+ {
1090
+ /* Connection mode button */
1091
+ }
1092
+ <button
1093
+ style={{
1094
+ position: 'absolute',
1095
+ top: 20,
1096
+ left: 20,
1097
+ zIndex: 10,
1098
+ }}
1099
+ onClick={() => setConnectionMode(!connectionMode)}
1100
+ >
1101
+ {connectionMode ? '✖ Cancel Connection' : '🔗 Connect Nodes'}
1102
+ </button>;
1103
+ ```
1104
+
1105
+ **Why:** Keeps controls accessible without interfering with canvas pan/zoom.
1106
+
1107
+ ---
1108
+
1109
+ ## Common Gotchas
1110
+
1111
+ ### 1. Container Height Collapsing
1112
+
1113
+ **Problem:** Canvas doesn't appear or has 0 height.
1114
+
1115
+ **Solution:** Ensure parent has explicit height:
1116
+
1117
+ ```tsx
1118
+ // Add height: '100vh' to root container
1119
+ <div style={{ height: '100vh' }}>
1120
+ <PIDCanvas ... />
1121
+ </div>
1122
+ ```
1123
+
1124
+ ### 2. Node Positions After Zoom
1125
+
1126
+ **Problem:** Dropped symbols appear at wrong location when zoomed.
1127
+
1128
+ **Solution:** Already handled! `screenToWorld()` accounts for viewBox and aspect ratio. Just use:
1129
+
1130
+ ```tsx
1131
+ onSymbolDrop={(symbolId, position) => {
1132
+ // position.x/y are already in world coordinates
1133
+ const node = { transform: { x: position.x, y: position.y } };
1134
+ }}
1135
+ ```
1136
+
1137
+ ### 3. Port Indicators Not Visible at Low Zoom
1138
+
1139
+ **Problem:** Port circles (5px radius) become tiny when zoomed out.
1140
+
1141
+ **Solution:** Indicators are SVG elements that scale with zoom. If needed, adjust radius:
1142
+
1143
+ ```tsx
1144
+ // Port indicators are sized in world units, not pixels
1145
+ // At viewBox scale 1:1, radius 5 = 5 screen pixels
1146
+ // At 50% zoom, radius 5 = 2.5 screen pixels
1147
+ ```
1148
+
1149
+ For zoom-independent sizing, consider adding a feature request for screen-space indicators.
1150
+
1151
+ ### 4. Pipe Routes Not Updating After Node Rotation
1152
+
1153
+ **Problem:** Rotating a node doesn't update connected pipe endpoints.
1154
+
1155
+ **Solution:** Call `onDiagramChange` or manually recalculate routes:
1156
+
1157
+ ```tsx
1158
+ import { generateRoutePoints } from '@procaaso/alphinity-ui-components';
1159
+
1160
+ const updatedPipes = diagram.pipes.map((pipe) => {
1161
+ const source = diagram.nodes.find((n) => n.id === pipe.fromNodeId);
1162
+ const target = diagram.nodes.find((n) => n.id === pipe.toNodeId);
1163
+
1164
+ return {
1165
+ ...pipe,
1166
+ routePoints: generateRoutePoints(source, pipe.fromPortId, target, pipe.toPortId),
1167
+ };
1168
+ });
1169
+ ```
1170
+
1171
+ Library auto-handles this for node dragging, but not for rotation (yet).
1172
+
1173
+ ### 5. Telemetry Updates Causing Performance Issues
1174
+
1175
+ **Problem:** WebSocket sends 100 updates/second, UI lags.
1176
+
1177
+ **Solution:** Throttle updates in your data hook:
1178
+
1179
+ ```tsx
1180
+ import { throttle } from 'lodash';
1181
+
1182
+ const updateTelemetry = throttle((data) => {
1183
+ setTelemetry(data);
1184
+ }, 500); // Max 2 updates/second
1185
+ ```
1186
+
1187
+ ### 6. Multiple Connection Mode Buttons
1188
+
1189
+ **Problem:** User clicks "Connect" again while already connecting.
1190
+
1191
+ **Solution:** Show different UI states:
1192
+
1193
+ ```tsx
1194
+ <button
1195
+ onClick={() => setConnectionMode(!connectionMode)}
1196
+ style={{
1197
+ backgroundColor: connectionMode ? '#ef4444' : '#3b82f6',
1198
+ }}
1199
+ >
1200
+ {connectionMode ? '✖ Cancel Connection' : '🔗 Connect Nodes'}
1201
+ </button>;
1202
+
1203
+ {
1204
+ connectionMode && <p style={{ marginTop: 8 }}>Click first node to start connection...</p>;
1205
+ }
1206
+ ```
1207
+
1208
+ ---
1209
+
1210
+ ## Migration from Demo
1211
+
1212
+ ```typescript
1213
+ // Example Express.js routes
1214
+ app.get('/api/diagrams/:id', async (req, res) => {
1215
+ const diagram = await db.diagrams.findById(req.params.id);
1216
+ res.json(diagram);
1217
+ });
1218
+
1219
+ app.put('/api/diagrams/:id', async (req, res) => {
1220
+ const diagram = req.body;
1221
+ await db.diagrams.update(req.params.id, diagram);
1222
+ res.json({ success: true });
1223
+ });
1224
+
1225
+ // WebSocket for real-time data
1226
+ wss.on('connection', (ws) => {
1227
+ const interval = setInterval(() => {
1228
+ // Fetch latest values from SCADA/PLC
1229
+ const data = {
1230
+ 'PLC1.Tank01.Temp': { value: 72.5, timestamp: new Date(), quality: 'good' },
1231
+ 'PLC1.Tank01.Level': { value: 45.2, timestamp: new Date(), quality: 'good' },
1232
+ };
1233
+ ws.send(JSON.stringify(data));
1234
+ }, 1000);
1235
+
1236
+ ws.on('close', () => clearInterval(interval));
1237
+ });
1238
+ ```
1239
+
1240
+ ---
1241
+
1242
+ ## API Backend Example
1243
+
1244
+ ```typescript
1245
+ // Example Express.js routes
1246
+ app.get('/api/diagrams/:id', async (req, res) => {
1247
+ const diagram = await db.diagrams.findById(req.params.id);
1248
+ res.json(diagram);
1249
+ });
1250
+
1251
+ app.put('/api/diagrams/:id', async (req, res) => {
1252
+ const diagram = req.body;
1253
+ await db.diagrams.update(req.params.id, diagram);
1254
+ res.json({ success: true });
1255
+ });
1256
+
1257
+ // WebSocket for real-time data
1258
+ wss.on('connection', (ws) => {
1259
+ const interval = setInterval(() => {
1260
+ // Fetch latest values from SCADA/PLC
1261
+ const data = {
1262
+ 'PLC1.Tank01.Temp': { value: 72.5, timestamp: new Date(), quality: 'good' },
1263
+ 'PLC1.Tank01.Level': { value: 45.2, timestamp: new Date(), quality: 'good' },
1264
+ };
1265
+ ws.send(JSON.stringify(data));
1266
+ }, 1000);
1267
+
1268
+ ws.on('close', () => clearInterval(interval));
1269
+ });
1270
+ ```
1271
+
1272
+ ---
1273
+
1274
+ ## Performance Tuning
1275
+
1276
+ ### Large Diagrams (100+ Nodes)
1277
+
1278
+ **Symptom:** Lag during pan/zoom or slow rendering.
1279
+
1280
+ **Solutions:**
1281
+
1282
+ 1. **Limit telemetry updates:** Max 1-2 Hz per node
1283
+ 2. **Debounce auto-save:** Wait for user to stop editing
1284
+ 3. **Lazy load diagrams:** Load summary list, full diagram on demand
1285
+ 4. **Consider pagination:** Split large process units across multiple diagrams
1286
+
1287
+ ### High-Frequency Data
1288
+
1289
+ **Symptom:** CPU spikes, choppy animation.
1290
+
1291
+ **Solutions:**
1292
+
1293
+ ```tsx
1294
+ // Throttle incoming WebSocket messages
1295
+ const throttledUpdate = throttle((data) => {
1296
+ updateTelemetry(data);
1297
+ }, 500); // 2 updates/sec max
1298
+
1299
+ ws.onmessage = (event) => {
1300
+ throttledUpdate(JSON.parse(event.data));
1301
+ };
1302
+ ```
1303
+
1304
+ ### Memory Leaks
1305
+
1306
+ **Symptom:** Browser memory grows over time in runtime mode.
1307
+
1308
+ **Solutions:**
1309
+
1310
+ ```tsx
1311
+ // Clean up WebSocket connections
1312
+ useEffect(() => {
1313
+ const ws = new WebSocket('...');
1314
+
1315
+ return () => {
1316
+ ws.close(); // Important!
1317
+ };
1318
+ }, []);
1319
+
1320
+ // Clean up intervals
1321
+ useEffect(() => {
1322
+ const interval = setInterval(...);
1323
+
1324
+ return () => {
1325
+ clearInterval(interval); // Important!
1326
+ };
1327
+ }, []);
1328
+ ```
1329
+
1330
+ ---
1331
+
1332
+ ## Migration from Demo
1333
+
1334
+ The `RuntimeDemo.tsx` file demonstrates all recommended patterns. To migrate to production:
1335
+
1336
+ 1. **Extract state management** → Create `useDiagramStorage` hook
1337
+ 2. **Move mode switching** → Implement in your app's routing/layout
1338
+ 3. **Add persistence** → Integrate with your database API
1339
+ 4. **Add authentication** → Role-based feature flags (engineer vs operator)
1340
+ 5. **Connect real data** → Replace mock simulation with SCADA/OPC UA client
1341
+
1342
+ **The patterns in RuntimeDemo are production-ready** - they represent best practices for:
1343
+
1344
+ - Edit/runtime mode separation
1345
+ - Visual port selection
1346
+ - Three-panel layout
1347
+ - Auto-save with debouncing
1348
+ - Keyboard shortcuts
1349
+ - Connection mode state management
1350
+
1351
+ The library handles **"how to render and interact"**, your app handles **"what to show and where data comes from"**.