import { useState, useCallback } from 'react' import { toast } from 'sonner' import { Globe, Router, Network, Server, Layers, Box, Container, HardDrive, Cpu, Wifi, Camera, Printer, Monitor, PlugZap, Anchor, Package, Circle, Flame, type LucideIcon, } from 'lucide-react' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { useThemeStore } from '@/stores/themeStore' import { useCanvasStore } from '@/stores/canvasStore' import { THEMES } from '@/utils/themes' import { applyOpacity } from '@/utils/colorUtils' import type { NodeType, EdgeType, NodeTypeStyle, EdgeTypeStyle, CustomStyleDef, EdgePathStyle, } from '@/types' import { NODE_TYPE_LABELS, EDGE_TYPE_LABELS } from '@/types' // ── Node types exposed for custom style (skip groupRect/group) ─────────────── const EDITABLE_NODE_TYPES: NodeType[] = [ 'isp', 'router', 'firewall', 'switch', 'server', 'proxmox', 'vm', 'lxc', 'nas', 'iot', 'ap', 'camera', 'printer', 'computer', 'cpl', 'docker_host', 'docker_container', 'generic', ] const EDITABLE_EDGE_TYPES: EdgeType[] = ['ethernet', 'wifi', 'iot', 'vlan', 'virtual', 'cluster'] const NODE_ICONS: Record = { isp: Globe, router: Router, firewall: Flame, switch: Network, server: Server, proxmox: Layers, vm: Box, lxc: Container, nas: HardDrive, iot: Cpu, ap: Wifi, camera: Camera, printer: Printer, computer: Monitor, cpl: PlugZap, docker_host: Anchor, docker_container: Package, generic: Circle, } // ── Default style for a node type (from default theme) ───────────────────── function defaultNodeStyle(nodeType: NodeType): NodeTypeStyle { const accent = THEMES.default.colors.nodeAccents[nodeType] ?? THEMES.default.colors.nodeAccents.generic return { borderColor: accent.border, borderOpacity: 1, bgColor: THEMES.default.colors.nodeCardBackground, bgOpacity: 1, iconColor: accent.icon, iconOpacity: 1, width: 0, height: 0, } } function defaultEdgeStyle(edgeType: EdgeType): EdgeTypeStyle { return { color: THEMES.default.colors.edgeColors[edgeType], opacity: 1, pathStyle: 'bezier', animated: 'none', } } // ── Color + opacity row ────────────────────────────────────────────────────── interface ColorRowProps { label: string color: string opacity: number onColorChange: (v: string) => void onOpacityChange: (v: number) => void } function ColorRow({ label, color, opacity, onColorChange, onOpacityChange }: ColorRowProps) { return (
{label} onColorChange(e.target.value)} className="w-7 h-7 rounded cursor-pointer border border-[#30363d] bg-transparent p-0.5" />
onOpacityChange(parseFloat(e.target.value))} className="flex-1 h-1 accent-[#00d4ff]" /> {Math.round(opacity * 100)}%
) } // ── Node type editor ───────────────────────────────────────────────────────── interface NodeEditorProps { nodeType: NodeType style: NodeTypeStyle onChange: (s: NodeTypeStyle) => void onApplyToExisting: () => void } function NodeEditor({ nodeType, style, onChange, onApplyToExisting }: NodeEditorProps) { const set = useCallback((k: K, v: NodeTypeStyle[K]) => { onChange({ ...style, [k]: v }) }, [style, onChange]) return (
{NODE_TYPE_LABELS[nodeType]}
set('borderColor', v)} onOpacityChange={(v) => set('borderOpacity', v)} /> set('bgColor', v)} onOpacityChange={(v) => set('bgOpacity', v)} /> set('iconColor', v)} onOpacityChange={(v) => set('iconOpacity', v)} />
Default size
0 = auto (min 140 × 50 px, grows with content)
W set('width', parseInt(e.target.value) || 0)} className="w-20 h-7 text-xs bg-[#0d1117] border border-[#30363d] rounded px-2 text-[#e6edf3]" />
H set('height', parseInt(e.target.value) || 0)} className="w-20 h-7 text-xs bg-[#0d1117] border border-[#30363d] rounded px-2 text-[#e6edf3]" />
) } // ── Edge type editor ───────────────────────────────────────────────────────── interface EdgeEditorProps { edgeType: EdgeType style: EdgeTypeStyle onChange: (s: EdgeTypeStyle) => void onApplyToExisting: () => void } function EdgeEditor({ edgeType, style, onChange, onApplyToExisting }: EdgeEditorProps) { const set = useCallback((k: K, v: EdgeTypeStyle[K]) => { onChange({ ...style, [k]: v }) }, [style, onChange]) return (
{EDGE_TYPE_LABELS[edgeType]}
set('color', v)} onOpacityChange={(v) => set('opacity', v)} />
Path style
{(['bezier', 'smooth'] as EdgePathStyle[]).map((ps) => ( ))}
Animation
) } // ── Main modal ─────────────────────────────────────────────────────────────── type Tab = 'nodes' | 'edges' type Selection = { kind: 'node'; type: NodeType } | { kind: 'edge'; type: EdgeType } | null interface CustomStyleModalProps { open: boolean onClose: () => void } export function CustomStyleModal({ open, onClose }: CustomStyleModalProps) { const { customStyle, setCustomStyle } = useThemeStore() const { markUnsaved, applyTypeNodeStyle, applyTypeEdgeStyle, applyAllCustomStyles } = useCanvasStore() const [tab, setTab] = useState('nodes') const [selection, setSelection] = useState(null) const [draft, setDraft] = useState(() => ({ nodes: { ...customStyle.nodes }, edges: { ...customStyle.edges }, })) const handleOpen = (isOpen: boolean) => { if (isOpen) { // Reset draft to current saved customStyle on open setDraft({ nodes: { ...customStyle.nodes }, edges: { ...customStyle.edges } }) setSelection(null) } else { onClose() } } const getNodeStyle = (t: NodeType): NodeTypeStyle => draft.nodes[t] ?? defaultNodeStyle(t) const getEdgeStyle = (t: EdgeType): EdgeTypeStyle => draft.edges[t] ?? defaultEdgeStyle(t) const handleNodeChange = (t: NodeType, s: NodeTypeStyle) => setDraft((d) => ({ ...d, nodes: { ...d.nodes, [t]: s } })) const handleEdgeChange = (t: EdgeType, s: EdgeTypeStyle) => setDraft((d) => ({ ...d, edges: { ...d.edges, [t]: s } })) const handleApplyNodeType = (t: NodeType) => { const style = getNodeStyle(t) applyTypeNodeStyle(t, style) toast.success(`Applied style to all ${NODE_TYPE_LABELS[t]} nodes`) } const handleApplyEdgeType = (t: EdgeType) => { const style = getEdgeStyle(t) applyTypeEdgeStyle(t, style) toast.success(`Applied style to all ${EDGE_TYPE_LABELS[t]} edges`) } const handleSave = () => { setCustomStyle(draft) markUnsaved() toast.success('Custom style saved — save your canvas to persist') onClose() } const handleApplyAll = () => { setCustomStyle(draft) applyAllCustomStyles(draft) markUnsaved() toast.success('Custom style applied to all nodes and edges') onClose() } const selectedNodeStyle = selection?.kind === 'node' ? getNodeStyle(selection.type) : null const selectedEdgeStyle = selection?.kind === 'edge' ? getEdgeStyle(selection.type) : null return ( Custom Style Editor
{/* Left panel — type list */}
{/* Tabs */}
{(['nodes', 'edges'] as Tab[]).map((t) => ( ))}
{/* Type list */}
{tab === 'nodes' && EDITABLE_NODE_TYPES.map((t) => { const Icon = NODE_ICONS[t] ?? Circle const style = draft.nodes[t] const isSelected = selection?.kind === 'node' && selection.type === t const swatchColor = style ? applyOpacity(style.borderColor, style.borderOpacity) : THEMES.default.colors.nodeAccents[t]?.border ?? '#8b949e' return ( ) })} {tab === 'edges' && EDITABLE_EDGE_TYPES.map((t) => { const style = draft.edges[t] const isSelected = selection?.kind === 'edge' && selection.type === t const swatchColor = style ? applyOpacity(style.color, style.opacity) : THEMES.default.colors.edgeColors[t] return ( ) })}
{/* Right panel — editor */}
{!selection && (
Select a {tab === 'nodes' ? 'node type' : 'edge type'} from the list to edit its style
)} {selection?.kind === 'node' && selectedNodeStyle && ( handleNodeChange(selection.type, s)} onApplyToExisting={() => handleApplyNodeType(selection.type)} /> )} {selection?.kind === 'edge' && selectedEdgeStyle && ( handleEdgeChange(selection.type, s)} onApplyToExisting={() => handleApplyEdgeType(selection.type)} /> )}
{/* Footer */}
) }