feat: multi-design canvas system with electrical nodes/edges

Backend: New Design model + designs table; design_id FK on nodes, edges, canvas_state; migration seeds default 'Network Topology' design; full CRUD API for designs; canvas load/save accept design_id.

Frontend: designStore (Zustand), design switcher in Sidebar, design-aware canvas load/save, auto-save on design switch.

Electrical node types (14): grid, ups, battery, generator, solar_panel, inverter, circuit_breaker, contactor, electrical_switch, socket, light, meter, transformer, load — icons, registrations, accent colors in all 6 themes.

Electrical edge type: registered in edgeTypes, BASE_STYLES, edgeColors, all theme edgeColors, EDGE_DEFAULT_COLORS.

Bug fixes: data corruption on design switch (stale closure), race condition on save-then-load, missing Zap import, missing Electrical group in NodeModal, missing electrical entries in custom theme edgeColors, inline imports hoisted.
This commit is contained in:
Pranjal Joshi
2026-05-31 14:35:22 +05:30
parent 4ba04660c8
commit 46435605eb
19 changed files with 616 additions and 61 deletions
+1
View File
@@ -8,4 +8,5 @@ export const EDGE_DEFAULT_COLORS: Record<EdgeType, string> = {
virtual: '#8b949e',
cluster: '#ff6e00',
fibre: '#22d3ee',
electrical: '#e3b341',
}
+99 -9
View File
@@ -68,6 +68,20 @@ export const THEMES: Record<ThemeId, ThemePreset> = {
groupRect: { border: '#00d4ff', icon: '#00d4ff' },
group: { border: '#00d4ff', icon: '#00d4ff' },
text: { border: '#30363d', icon: '#e6edf3' },
grid: { border: '#ff6e00', icon: '#ff6e00' },
ups: { border: '#39d353', icon: '#39d353' },
battery: { border: '#39d353', icon: '#39d353' },
generator: { border: '#e3b341', icon: '#e3b341' },
solar_panel: { border: '#e3b341', icon: '#e3b341' },
inverter: { border: '#a855f7', icon: '#a855f7' },
circuit_breaker: { border: '#f85149', icon: '#f85149' },
contactor: { border: '#f85149', icon: '#f85149' },
electrical_switch: { border: '#00d4ff', icon: '#00d4ff' },
socket: { border: '#8b949e', icon: '#8b949e' },
light: { border: '#e3b341', icon: '#e3b341' },
meter: { border: '#00d4ff', icon: '#00d4ff' },
transformer: { border: '#a855f7', icon: '#a855f7' },
load: { border: '#ec4899', icon: '#ec4899' },
},
nodeCardBackground: '#21262d',
nodeIconBackground: '#161b22',
@@ -87,6 +101,7 @@ export const THEMES: Record<ThemeId, ThemePreset> = {
virtual: '#8b949e',
cluster: '#ff6e00',
fibre: '#22d3ee',
electrical:'#e3b341',
},
edgeSelectedColor: '#00d4ff',
edgeLabelBackground:'#161b22',
@@ -132,6 +147,20 @@ export const THEMES: Record<ThemeId, ThemePreset> = {
groupRect: { border: '#22d3ee', icon: '#22d3ee' },
group: { border: '#22d3ee', icon: '#22d3ee' },
text: { border: '#404040', icon: '#ffffff' },
grid: { border: '#ff6e00', icon: '#ff6e00' },
ups: { border: '#39d353', icon: '#39d353' },
battery: { border: '#39d353', icon: '#39d353' },
generator: { border: '#e3b341', icon: '#e3b341' },
solar_panel: { border: '#e3b341', icon: '#e3b341' },
inverter: { border: '#a855f7', icon: '#a855f7' },
circuit_breaker: { border: '#f85149', icon: '#f85149' },
contactor: { border: '#f85149', icon: '#f85149' },
electrical_switch: { border: '#00d4ff', icon: '#00d4ff' },
socket: { border: '#8b949e', icon: '#8b949e' },
light: { border: '#e3b341', icon: '#e3b341' },
meter: { border: '#00d4ff', icon: '#00d4ff' },
transformer: { border: '#a855f7', icon: '#a855f7' },
load: { border: '#ec4899', icon: '#ec4899' },
},
nodeCardBackground: '#0a0a0a',
nodeIconBackground: '#111111',
@@ -151,15 +180,16 @@ export const THEMES: Record<ThemeId, ThemePreset> = {
virtual: '#6b7280',
cluster: '#fb923c',
fibre: '#06b6d4',
electrical:'#e3b341',
},
edgeSelectedColor: '#22d3ee',
edgeLabelBackground:'#111111',
edgeLabelColor: '#666666',
edgeLabelBorder: '#1c1c1e',
canvasBackground: '#000000',
canvasDotColor: '#1a1a1a',
handleBackground: '#1c1c1e',
handleBorder: '#444444',
edgeLabelBackground:'#161b22',
edgeLabelColor: '#9ca3af',
edgeLabelBorder: '#374151',
canvasBackground: '#030712',
canvasDotColor: '#374151',
handleBackground: '#374151',
handleBorder: '#9ca3af',
reactFlowColorMode: 'dark',
},
},
@@ -196,6 +226,20 @@ export const THEMES: Record<ThemeId, ThemePreset> = {
groupRect: { border: '#0284c7', icon: '#0284c7' },
group: { border: '#0284c7', icon: '#0284c7' },
text: { border: '#cbd5e1', icon: '#1f2328' },
grid: { border: '#ff6e00', icon: '#ff6e00' },
ups: { border: '#39d353', icon: '#39d353' },
battery: { border: '#39d353', icon: '#39d353' },
generator: { border: '#e3b341', icon: '#e3b341' },
solar_panel: { border: '#e3b341', icon: '#e3b341' },
inverter: { border: '#a855f7', icon: '#a855f7' },
circuit_breaker: { border: '#f85149', icon: '#f85149' },
contactor: { border: '#f85149', icon: '#f85149' },
electrical_switch: { border: '#00d4ff', icon: '#00d4ff' },
socket: { border: '#8b949e', icon: '#8b949e' },
light: { border: '#e3b341', icon: '#e3b341' },
meter: { border: '#00d4ff', icon: '#00d4ff' },
transformer: { border: '#a855f7', icon: '#a855f7' },
load: { border: '#ec4899', icon: '#ec4899' },
},
nodeCardBackground: '#ffffff',
nodeIconBackground: '#f0f6ff',
@@ -215,15 +259,16 @@ export const THEMES: Record<ThemeId, ThemePreset> = {
virtual: '#9ca3af',
cluster: '#ea580c',
fibre: '#0891b2',
electrical:'#d97706',
},
edgeSelectedColor: '#0284c7',
edgeLabelBackground:'#ffffff',
edgeLabelColor: '#57606a',
edgeLabelColor: '#6b7280',
edgeLabelBorder: '#d0d7de',
canvasBackground: '#f6f8fa',
canvasDotColor: '#d0d7de',
handleBackground: '#d0d7de',
handleBorder: '#9ca3af',
handleBorder: '#6b7280',
reactFlowColorMode: 'light',
},
},
@@ -260,6 +305,20 @@ export const THEMES: Record<ThemeId, ThemePreset> = {
groupRect: { border: '#00ffff', icon: '#00ffff' },
group: { border: '#00ffff', icon: '#00ffff' },
text: { border: '#3a3a6a', icon: '#ffffff' },
grid: { border: '#ff6e00', icon: '#ff6e00' },
ups: { border: '#39d353', icon: '#39d353' },
battery: { border: '#39d353', icon: '#39d353' },
generator: { border: '#e3b341', icon: '#e3b341' },
solar_panel: { border: '#e3b341', icon: '#e3b341' },
inverter: { border: '#a855f7', icon: '#a855f7' },
circuit_breaker: { border: '#f85149', icon: '#f85149' },
contactor: { border: '#f85149', icon: '#f85149' },
electrical_switch: { border: '#00d4ff', icon: '#00d4ff' },
socket: { border: '#8b949e', icon: '#8b949e' },
light: { border: '#e3b341', icon: '#e3b341' },
meter: { border: '#00d4ff', icon: '#00d4ff' },
transformer: { border: '#a855f7', icon: '#a855f7' },
load: { border: '#ec4899', icon: '#ec4899' },
},
nodeCardBackground: '#0f0f2a',
nodeIconBackground: '#0a0a1a',
@@ -279,6 +338,7 @@ export const THEMES: Record<ThemeId, ThemePreset> = {
virtual: '#8888cc',
cluster: '#ff8800',
fibre: '#00e5ff',
electrical:'#ffff00',
},
edgeSelectedColor: '#00ffff',
edgeLabelBackground:'#0a0a1a',
@@ -324,6 +384,20 @@ export const THEMES: Record<ThemeId, ThemePreset> = {
groupRect: { border: '#00ff41', icon: '#00ff41' },
group: { border: '#00ff41', icon: '#00ff41' },
text: { border: '#003311', icon: '#00ff41' },
grid: { border: '#cc6600', icon: '#cc6600' },
ups: { border: '#00ff41', icon: '#00ff41' },
battery: { border: '#00ff41', icon: '#00ff41' },
generator: { border: '#ffcc00', icon: '#ffcc00' },
solar_panel: { border: '#ffcc00', icon: '#ffcc00' },
inverter: { border: '#aa00ff', icon: '#aa00ff' },
circuit_breaker: { border: '#ff0033', icon: '#ff0033' },
contactor: { border: '#ff0033', icon: '#ff0033' },
electrical_switch: { border: '#00d4ff', icon: '#00d4ff' },
socket: { border: '#005500', icon: '#005500' },
light: { border: '#ffcc00', icon: '#ffcc00' },
meter: { border: '#00d4ff', icon: '#00d4ff' },
transformer: { border: '#aa00ff', icon: '#aa00ff' },
load: { border: '#ff69b4', icon: '#ff69b4' },
},
nodeCardBackground: '#001100',
nodeIconBackground: '#002200',
@@ -343,6 +417,7 @@ export const THEMES: Record<ThemeId, ThemePreset> = {
virtual: '#004400',
cluster: '#33ff66',
fibre: '#00ffcc',
electrical:'#66ff33',
},
edgeSelectedColor: '#00ff41',
edgeLabelBackground:'#001100',
@@ -388,6 +463,20 @@ export const THEMES: Record<ThemeId, ThemePreset> = {
groupRect: { border: '#00d4ff', icon: '#00d4ff' },
group: { border: '#00d4ff', icon: '#00d4ff' },
text: { border: '#30363d', icon: '#e6edf3' },
grid: { border: '#ff6e00', icon: '#ff6e00' },
ups: { border: '#39d353', icon: '#39d353' },
battery: { border: '#39d353', icon: '#39d353' },
generator: { border: '#e3b341', icon: '#e3b341' },
solar_panel: { border: '#e3b341', icon: '#e3b341' },
inverter: { border: '#a855f7', icon: '#a855f7' },
circuit_breaker: { border: '#f85149', icon: '#f85149' },
contactor: { border: '#f85149', icon: '#f85149' },
electrical_switch: { border: '#00d4ff', icon: '#00d4ff' },
socket: { border: '#8b949e', icon: '#8b949e' },
light: { border: '#e3b341', icon: '#e3b341' },
meter: { border: '#00d4ff', icon: '#00d4ff' },
transformer: { border: '#a855f7', icon: '#a855f7' },
load: { border: '#ec4899', icon: '#ec4899' },
},
nodeCardBackground: '#21262d',
nodeIconBackground: '#161b22',
@@ -407,6 +496,7 @@ export const THEMES: Record<ThemeId, ThemePreset> = {
virtual: '#8b949e',
cluster: '#ff6e00',
fibre: '#22d3ee',
electrical: '#e3b341',
},
edgeSelectedColor: '#00d4ff',
edgeLabelBackground:'#161b22',