Mass-Restructure of JSX Folder Structure

This commit is contained in:
2025-09-02 20:07:29 -06:00
parent 0f412f7491
commit 4bbef112ec
11 changed files with 11 additions and 12 deletions

View File

@@ -0,0 +1,415 @@
import React, { useState, useEffect } from "react";
import { Box, Typography, Tabs, Tab, TextField, MenuItem, Button, Slider, IconButton, Tooltip } from "@mui/material";
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import ContentPasteIcon from "@mui/icons-material/ContentPaste";
import RestoreIcon from "@mui/icons-material/Restore";
import { SketchPicker } from "react-color";
const SIDEBAR_WIDTH = 400;
const DEFAULT_EDGE_STYLE = {
type: "bezier",
animated: true,
style: { strokeDasharray: "6 3", stroke: "#58a6ff", strokeWidth: 1 },
label: "",
labelStyle: { fill: "#fff", fontWeight: "bold" },
labelBgStyle: { fill: "#2c2c2c", fillOpacity: 0.85, rx: 16, ry: 16 },
labelBgPadding: [8, 4],
};
let globalEdgeClipboard = null;
function clone(obj) {
return JSON.parse(JSON.stringify(obj));
}
export default function Context_Menu_Sidebar({
open,
onClose,
edge,
updateEdge,
}) {
const [activeTab, setActiveTab] = useState(0);
const [editState, setEditState] = useState(() => (edge ? clone(edge) : {}));
const [colorPicker, setColorPicker] = useState({ field: null, anchor: null });
useEffect(() => {
if (edge && edge.id !== editState.id) setEditState(clone(edge));
// eslint-disable-next-line
}, [edge]);
const handleChange = (field, value) => {
setEditState((prev) => {
const updated = { ...prev };
if (field === "label") updated.label = value;
else if (field === "labelStyle.fill") updated.labelStyle = { ...updated.labelStyle, fill: value };
else if (field === "labelBgStyle.fill") updated.labelBgStyle = { ...updated.labelBgStyle, fill: value };
else if (field === "labelBgStyle.rx") updated.labelBgStyle = { ...updated.labelBgStyle, rx: value, ry: value };
else if (field === "labelBgPadding") updated.labelBgPadding = value;
else if (field === "labelBgStyle.fillOpacity") updated.labelBgStyle = { ...updated.labelBgStyle, fillOpacity: value };
else if (field === "type") updated.type = value;
else if (field === "animated") updated.animated = value;
else if (field === "style.stroke") updated.style = { ...updated.style, stroke: value };
else if (field === "style.strokeDasharray") updated.style = { ...updated.style, strokeDasharray: value };
else if (field === "style.strokeWidth") updated.style = { ...updated.style, strokeWidth: value };
else if (field === "labelStyle.fontWeight") updated.labelStyle = { ...updated.labelStyle, fontWeight: value };
else updated[field] = value;
if (field === "style.strokeDasharray") {
if (value === "") {
updated.animated = false;
updated.style = { ...updated.style, strokeDasharray: "" };
} else {
updated.animated = true;
updated.style = { ...updated.style, strokeDasharray: value };
}
}
updateEdge({ ...updated, id: prev.id });
return updated;
});
};
// Color Picker with right alignment
const openColorPicker = (field, event) => {
setColorPicker({ field, anchor: event.currentTarget });
};
const closeColorPicker = () => {
setColorPicker({ field: null, anchor: null });
};
const handleColorChange = (color) => {
handleChange(colorPicker.field, color.hex);
closeColorPicker();
};
// Reset, Copy, Paste logic
const handleReset = () => {
setEditState(clone({ ...DEFAULT_EDGE_STYLE, id: edge.id }));
updateEdge({ ...DEFAULT_EDGE_STYLE, id: edge.id });
};
const handleCopy = () => { globalEdgeClipboard = clone(editState); };
const handlePaste = () => {
if (globalEdgeClipboard) {
setEditState(clone({ ...globalEdgeClipboard, id: edge.id }));
updateEdge({ ...globalEdgeClipboard, id: edge.id });
}
};
const renderColorButton = (label, field, value) => (
<span style={{ display: "inline-block", verticalAlign: "middle", position: "relative" }}>
<Button
variant="outlined"
size="small"
onClick={(e) => openColorPicker(field, e)}
sx={{
ml: 1,
borderColor: "#444",
color: "#ccc",
minWidth: 0,
width: 32,
height: 24,
p: 0,
bgcolor: "#232323",
}}
>
<span style={{
display: "inline-block",
width: 20,
height: 16,
background: value,
borderRadius: 3,
border: "1px solid #888",
}} />
</Button>
{colorPicker.field === field && (
<Box sx={{
position: "absolute",
top: "32px",
right: 0,
zIndex: 1302,
boxShadow: "0 2px 16px rgba(0,0,0,0.24)"
}}>
<SketchPicker
color={value}
onChange={handleColorChange}
disableAlpha
presetColors={[
"#fff", "#000", "#58a6ff", "#ff4f4f", "#2c2c2c", "#00d18c",
"#e3e3e3", "#0475c2", "#ff8c00", "#6b21a8", "#0e7490"
]}
/>
</Box>
)}
</span>
);
// Label tab
const renderLabelTab = () => (
<Box sx={{ px: 2, pt: 1, pb: 2 }}>
<Box sx={{ display: "flex", alignItems: "center", mb: 1 }}>
<Typography variant="body2" sx={{ color: "#ccc", flex: 1 }}>Label</Typography>
</Box>
<TextField
fullWidth
size="small"
variant="outlined"
value={editState.label || ""}
onChange={e => handleChange("label", e.target.value)}
sx={{
mb: 2,
input: { color: "#fff", bgcolor: "#1e1e1e", fontSize: "0.95rem" },
"& fieldset": { borderColor: "#444" },
}}
/>
<Box sx={{ display: "flex", alignItems: "center", mb: 1 }}>
<Typography variant="body2" sx={{ color: "#ccc", flex: 1 }}>Text Color</Typography>
{renderColorButton("Label Text Color", "labelStyle.fill", editState.labelStyle?.fill || "#fff")}
</Box>
<Box sx={{ display: "flex", alignItems: "center", mb: 1 }}>
<Typography variant="body2" sx={{ color: "#ccc", flex: 1 }}>Background</Typography>
{renderColorButton("Label Background Color", "labelBgStyle.fill", editState.labelBgStyle?.fill || "#2c2c2c")}
</Box>
<Box sx={{ display: "flex", alignItems: "center", mb: 2 }}>
<Typography variant="body2" sx={{ color: "#ccc", flex: 1 }}>Padding</Typography>
<TextField
size="small"
type="text"
value={editState.labelBgPadding ? editState.labelBgPadding.join(",") : "8,4"}
onChange={e => {
const val = e.target.value.split(",").map(x => parseInt(x.trim())).filter(x => !isNaN(x));
if (val.length === 2) handleChange("labelBgPadding", val);
}}
sx={{ width: 80, input: { color: "#fff", bgcolor: "#1e1e1e", fontSize: "0.95rem" } }}
/>
</Box>
<Box sx={{ display: "flex", alignItems: "center", mb: 2 }}>
<Typography variant="body2" sx={{ color: "#ccc", flex: 1 }}>Background Style</Typography>
<TextField
select
size="small"
value={(editState.labelBgStyle?.rx ?? 11) >= 11 ? "rounded" : "square"}
onChange={e => {
handleChange("labelBgStyle.rx", e.target.value === "rounded" ? 11 : 0);
}}
sx={{
width: 150,
bgcolor: "#1e1e1e",
"& .MuiSelect-select": { color: "#fff" }
}}
>
<MenuItem value="rounded">Rounded</MenuItem>
<MenuItem value="square">Square</MenuItem>
</TextField>
</Box>
<Box sx={{ display: "flex", alignItems: "center", mb: 2 }}>
<Typography variant="body2" sx={{ color: "#ccc", flex: 1 }}>Background Opacity</Typography>
<Slider
value={editState.labelBgStyle?.fillOpacity ?? 0.85}
min={0}
max={1}
step={0.05}
onChange={(_, v) => handleChange("labelBgStyle.fillOpacity", v)}
sx={{ width: 100, ml: 2 }}
/>
<TextField
size="small"
type="number"
value={editState.labelBgStyle?.fillOpacity ?? 0.85}
onChange={e => handleChange("labelBgStyle.fillOpacity", parseFloat(e.target.value) || 0)}
sx={{ width: 60, ml: 2, input: { color: "#fff", bgcolor: "#1e1e1e", fontSize: "0.95rem" } }}
/>
</Box>
</Box>
);
const renderStyleTab = () => (
<Box sx={{ px: 2, pt: 1, pb: 2 }}>
<Box sx={{ display: "flex", alignItems: "center", mb: 2 }}>
<Typography variant="body2" sx={{ color: "#ccc", flex: 1 }}>Edge Style</Typography>
<TextField
select
size="small"
value={editState.type || "bezier"}
onChange={e => handleChange("type", e.target.value)}
sx={{
width: 200,
bgcolor: "#1e1e1e",
"& .MuiSelect-select": { color: "#fff" }
}}
>
<MenuItem value="step">Step</MenuItem>
<MenuItem value="bezier">Curved (Bezier)</MenuItem>
<MenuItem value="straight">Straight</MenuItem>
<MenuItem value="smoothstep">Smoothstep</MenuItem>
</TextField>
</Box>
<Box sx={{ display: "flex", alignItems: "center", mb: 2 }}>
<Typography variant="body2" sx={{ color: "#ccc", flex: 1 }}>Edge Animation</Typography>
<TextField
select
size="small"
value={
editState.style?.strokeDasharray === "6 3" ? "dashes"
: editState.style?.strokeDasharray === "2 4" ? "dots"
: "solid"
}
onChange={e => {
const val = e.target.value;
handleChange("style.strokeDasharray",
val === "dashes" ? "6 3" :
val === "dots" ? "2 4" : ""
);
}}
sx={{
width: 200,
bgcolor: "#1e1e1e",
"& .MuiSelect-select": { color: "#fff" }
}}
>
<MenuItem value="dashes">Dashes</MenuItem>
<MenuItem value="dots">Dots</MenuItem>
<MenuItem value="solid">Solid</MenuItem>
</TextField>
</Box>
<Box sx={{ display: "flex", alignItems: "center", mb: 2 }}>
<Typography variant="body2" sx={{ color: "#ccc", flex: 1 }}>Color</Typography>
{renderColorButton("Edge Color", "style.stroke", editState.style?.stroke || "#58a6ff")}
</Box>
<Box sx={{ display: "flex", alignItems: "center", mb: 2 }}>
<Typography variant="body2" sx={{ color: "#ccc", flex: 1 }}>Edge Width</Typography>
<Slider
value={editState.style?.strokeWidth ?? 2}
min={1}
max={10}
step={1}
onChange={(_, v) => handleChange("style.strokeWidth", v)}
sx={{ width: 100, ml: 2 }}
/>
<TextField
size="small"
type="number"
value={editState.style?.strokeWidth ?? 2}
onChange={e => handleChange("style.strokeWidth", parseInt(e.target.value) || 1)}
sx={{ width: 60, ml: 2, input: { color: "#fff", bgcolor: "#1e1e1e", fontSize: "0.95rem" } }}
/>
</Box>
</Box>
);
// Always render the sidebar for animation!
if (!edge) return null;
return (
<>
{/* Overlay */}
<Box
onClick={onClose}
sx={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(0, 0, 0, 0.3)",
opacity: open ? 1 : 0,
pointerEvents: open ? "auto" : "none",
transition: "opacity 0.6s ease",
zIndex: 10
}}
/>
{/* Sidebar */}
<Box
sx={{
position: "absolute",
top: 0,
right: 0,
bottom: 0,
width: SIDEBAR_WIDTH,
bgcolor: "#2C2C2C",
color: "#ccc",
borderLeft: "1px solid #333",
padding: 0,
zIndex: 11,
display: "flex",
flexDirection: "column",
height: "100%",
transform: open ? "translateX(0)" : `translateX(${SIDEBAR_WIDTH}px)`,
transition: "transform 0.3s cubic-bezier(.77,0,.18,1)"
}}
onClick={e => e.stopPropagation()}
>
<Box sx={{ backgroundColor: "#232323", borderBottom: "1px solid #333" }}>
<Box sx={{ padding: "12px 16px", display: "flex", alignItems: "center" }}>
<Typography variant="h7" sx={{ color: "#0475c2", fontWeight: "bold", flex: 1 }}>
Edit Edge Properties
</Typography>
</Box>
<Tabs
value={activeTab}
onChange={(_, v) => setActiveTab(v)}
variant="fullWidth"
textColor="inherit"
TabIndicatorProps={{ style: { backgroundColor: "#ccc" } }}
sx={{
borderTop: "1px solid #333",
borderBottom: "1px solid #333",
minHeight: "36px",
height: "36px"
}}
>
<Tab label="Label" sx={{
color: "#ccc",
"&.Mui-selected": { color: "#ccc" },
minHeight: "36px",
height: "36px",
textTransform: "none"
}} />
<Tab label="Style" sx={{
color: "#ccc",
"&.Mui-selected": { color: "#ccc" },
minHeight: "36px",
height: "36px",
textTransform: "none"
}} />
</Tabs>
</Box>
{/* Main fields scrollable */}
<Box sx={{ flex: 1, overflowY: "auto" }}>
{activeTab === 0 && renderLabelTab()}
{activeTab === 1 && renderStyleTab()}
</Box>
{/* Sticky footer bar */}
<Box sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
px: 2, py: 1,
borderTop: "1px solid #333",
backgroundColor: "#232323",
flexShrink: 0
}}>
<Box>
<Tooltip title="Copy Style"><IconButton onClick={handleCopy}><ContentCopyIcon /></IconButton></Tooltip>
<Tooltip title="Paste Style"><IconButton onClick={handlePaste}><ContentPasteIcon /></IconButton></Tooltip>
</Box>
<Box>
<Tooltip title="Reset to Default"><Button variant="outlined" size="small" startIcon={<RestoreIcon />} onClick={handleReset} sx={{
color: "#58a6ff", borderColor: "#58a6ff", textTransform: "none"
}}>Reset to Default</Button></Tooltip>
</Box>
</Box>
</Box>
</>
);
}

View File

@@ -0,0 +1,374 @@
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/Flow_Editor.jsx
// Import Node Configuration Sidebar and new Context Menu Sidebar
import NodeConfigurationSidebar from "./Node_Configuration_Sidebar";
import ContextMenuSidebar from "./Context_Menu_Sidebar";
import React, { useState, useEffect, useCallback, useRef } from "react";
import ReactFlow, {
Background,
addEdge,
applyNodeChanges,
applyEdgeChanges,
useReactFlow
} from "reactflow";
import { Menu, MenuItem, Box } from "@mui/material";
import {
Polyline as PolylineIcon,
DeleteForever as DeleteForeverIcon,
Edit as EditIcon
} from "@mui/icons-material";
import "reactflow/dist/style.css";
export default function FlowEditor({
flowId,
nodes,
edges,
setNodes,
setEdges,
nodeTypes,
categorizedNodes
}) {
// Node Configuration Sidebar State
const [drawerOpen, setDrawerOpen] = useState(false);
const [selectedNodeId, setSelectedNodeId] = useState(null);
// Edge Properties Sidebar State
const [edgeSidebarOpen, setEdgeSidebarOpen] = useState(false);
const [edgeSidebarEdgeId, setEdgeSidebarEdgeId] = useState(null);
// Context Menus
const [nodeContextMenu, setNodeContextMenu] = useState(null); // { mouseX, mouseY, nodeId }
const [edgeContextMenu, setEdgeContextMenu] = useState(null); // { mouseX, mouseY, edgeId }
// Drag/snap helpers (untouched)
const wrapperRef = useRef(null);
const { project } = useReactFlow();
const [guides, setGuides] = useState([]);
const [activeGuides, setActiveGuides] = useState([]);
const movingFlowSize = useRef({ width: 0, height: 0 });
// ----- Node/Edge Definitions -----
const selectedNode = nodes.find((n) => n.id === selectedNodeId);
const selectedEdge = edges.find((e) => e.id === edgeSidebarEdgeId);
// --------- Context Menu Handlers ----------
const handleRightClick = (e, node) => {
e.preventDefault();
setNodeContextMenu({ mouseX: e.clientX + 2, mouseY: e.clientY - 6, nodeId: node.id });
};
const handleEdgeRightClick = (e, edge) => {
e.preventDefault();
setEdgeContextMenu({ mouseX: e.clientX + 2, mouseY: e.clientY - 6, edgeId: edge.id });
};
// --------- Node Context Menu Actions ---------
const handleDisconnectAllEdges = (nodeId) => {
setEdges((eds) => eds.filter((e) => e.source !== nodeId && e.target !== nodeId));
setNodeContextMenu(null);
};
const handleRemoveNode = (nodeId) => {
setNodes((nds) => nds.filter((n) => n.id !== nodeId));
setEdges((eds) => eds.filter((e) => e.source !== nodeId && e.target !== nodeId));
setNodeContextMenu(null);
};
const handleEditNodeProps = (nodeId) => {
setSelectedNodeId(nodeId);
setDrawerOpen(true);
setNodeContextMenu(null);
};
// --------- Edge Context Menu Actions ---------
const handleUnlinkEdge = (edgeId) => {
setEdges((eds) => eds.filter((e) => e.id !== edgeId));
setEdgeContextMenu(null);
};
const handleEditEdgeProps = (edgeId) => {
setEdgeSidebarEdgeId(edgeId);
setEdgeSidebarOpen(true);
setEdgeContextMenu(null);
};
// ----- Sidebar Closing -----
const handleCloseNodeSidebar = () => {
setDrawerOpen(false);
setSelectedNodeId(null);
};
const handleCloseEdgeSidebar = () => {
setEdgeSidebarOpen(false);
setEdgeSidebarEdgeId(null);
};
// ----- Update Edge Callback for Sidebar -----
const updateEdge = (updatedEdgeObj) => {
setEdges((eds) =>
eds.map((e) => (e.id === updatedEdgeObj.id ? { ...e, ...updatedEdgeObj } : e))
);
};
// ----- Drag/Drop, Guides, Node Snap Logic (unchanged) -----
const computeGuides = useCallback((dragNode) => {
if (!wrapperRef.current) return;
const parentRect = wrapperRef.current.getBoundingClientRect();
const dragEl = wrapperRef.current.querySelector(
`.react-flow__node[data-id="${dragNode.id}"]`
);
if (dragEl) {
const dr = dragEl.getBoundingClientRect();
const relLeft = dr.left - parentRect.left;
const relTop = dr.top - parentRect.top;
const relRight = relLeft + dr.width;
const relBottom = relTop + dr.height;
const pTL = project({ x: relLeft, y: relTop });
const pTR = project({ x: relRight, y: relTop });
const pBL = project({ x: relLeft, y: relBottom });
movingFlowSize.current = { width: pTR.x - pTL.x, height: pBL.y - pTL.y };
}
const lines = [];
nodes.forEach((n) => {
if (n.id === dragNode.id) return;
const el = wrapperRef.current.querySelector(
`.react-flow__node[data-id="${n.id}"]`
);
if (!el) return;
const r = el.getBoundingClientRect();
const relLeft = r.left - parentRect.left;
const relTop = r.top - parentRect.top;
const relRight = relLeft + r.width;
const relBottom = relTop + r.height;
const pTL = project({ x: relLeft, y: relTop });
const pTR = project({ x: relRight, y: relTop });
const pBL = project({ x: relLeft, y: relBottom });
lines.push({ xFlow: pTL.x, xPx: relLeft });
lines.push({ xFlow: pTR.x, xPx: relRight });
lines.push({ yFlow: pTL.y, yPx: relTop });
lines.push({ yFlow: pBL.y, yPx: relBottom });
});
setGuides(lines);
}, [nodes, project]);
const onNodeDrag = useCallback((_, node) => {
const threshold = 5;
let snapX = null, snapY = null;
const show = [];
const { width: fw, height: fh } = movingFlowSize.current;
guides.forEach((ln) => {
if (ln.xFlow != null) {
if (Math.abs(node.position.x - ln.xFlow) < threshold) { snapX = ln.xFlow; show.push({ xPx: ln.xPx }); }
else if (Math.abs(node.position.x + fw - ln.xFlow) < threshold) { snapX = ln.xFlow - fw; show.push({ xPx: ln.xPx }); }
}
if (ln.yFlow != null) {
if (Math.abs(node.position.y - ln.yFlow) < threshold) { snapY = ln.yFlow; show.push({ yPx: ln.yPx }); }
else if (Math.abs(node.position.y + fh - ln.yFlow) < threshold) { snapY = ln.yFlow - fh; show.push({ yPx: ln.yPx }); }
}
});
if (snapX !== null || snapY !== null) {
setNodes((nds) =>
applyNodeChanges(
[{
id: node.id,
type: "position",
position: {
x: snapX !== null ? snapX : node.position.x,
y: snapY !== null ? snapY : node.position.y
}
}],
nds
)
);
setActiveGuides(show);
} else {
setActiveGuides([]);
}
}, [guides, setNodes]);
const onDrop = useCallback((event) => {
event.preventDefault();
const type = event.dataTransfer.getData("application/reactflow");
if (!type) return;
const bounds = wrapperRef.current.getBoundingClientRect();
const position = project({
x: event.clientX - bounds.left,
y: event.clientY - bounds.top
});
const id = "node-" + Date.now();
const nodeMeta = Object.values(categorizedNodes).flat().find((n) => n.type === type);
// Seed config defaults:
const configDefaults = {};
(nodeMeta?.config || []).forEach(cfg => {
if (cfg.defaultValue !== undefined) {
configDefaults[cfg.key] = cfg.defaultValue;
}
});
const newNode = {
id,
type,
position,
data: {
label: nodeMeta?.label || type,
content: nodeMeta?.content,
...configDefaults
},
dragHandle: ".borealis-node-header"
};
setNodes((nds) => [...nds, newNode]);
}, [project, setNodes, categorizedNodes]);
const onDragOver = useCallback((event) => {
event.preventDefault();
event.dataTransfer.dropEffect = "move";
}, []);
const onConnect = useCallback((params) => {
setEdges((eds) =>
addEdge({
...params,
type: "bezier",
animated: true,
style: { strokeDasharray: "6 3", stroke: "#58a6ff" }
}, eds)
);
}, [setEdges]);
const onNodesChange = useCallback((changes) => {
setNodes((nds) => applyNodeChanges(changes, nds));
}, [setNodes]);
const onEdgesChange = useCallback((changes) => {
setEdges((eds) => applyEdgeChanges(changes, eds));
}, [setEdges]);
useEffect(() => {
const nodeCountEl = document.getElementById("nodeCount");
if (nodeCountEl) nodeCountEl.innerText = nodes.length;
}, [nodes]);
const nodeDef = selectedNode
? Object.values(categorizedNodes).flat().find((def) => def.type === selectedNode.type)
: null;
// --------- MAIN RENDER ----------
return (
<div
className="flow-editor-container"
ref={wrapperRef}
style={{ position: "relative" }}
>
{/* Node Config Sidebar */}
<NodeConfigurationSidebar
drawerOpen={drawerOpen}
setDrawerOpen={setDrawerOpen}
title={selectedNode ? selectedNode.data?.label || selectedNode.id : ""}
nodeData={
selectedNode && nodeDef
? {
config: nodeDef.config,
usage_documentation: nodeDef.usage_documentation,
...selectedNode.data,
nodeId: selectedNode.id
}
: null
}
setNodes={setNodes}
selectedNode={selectedNode}
/>
{/* Edge Properties Sidebar */}
<ContextMenuSidebar
open={edgeSidebarOpen}
onClose={handleCloseEdgeSidebar}
edge={selectedEdge ? { ...selectedEdge } : null}
updateEdge={edge => {
// Provide id if missing
if (!edge.id && edgeSidebarEdgeId) edge.id = edgeSidebarEdgeId;
updateEdge(edge);
}}
/>
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onDrop={onDrop}
onDragOver={onDragOver}
onNodeContextMenu={handleRightClick}
onEdgeContextMenu={handleEdgeRightClick}
defaultViewport={{ x: 0, y: 0, zoom: 1.5 }}
edgeOptions={{ type: "bezier", animated: true, style: { strokeDasharray: "6 3", stroke: "#58a6ff" } }}
proOptions={{ hideAttribution: true }}
onNodeDragStart={(_, node) => computeGuides(node)}
onNodeDrag={onNodeDrag}
onNodeDragStop={() => { setGuides([]); setActiveGuides([]); }}
>
<Background id={flowId} variant="lines" gap={65} size={1} color="rgba(255,255,255,0.2)" />
</ReactFlow>
{/* Helper lines for snapping */}
{activeGuides.map((ln, i) =>
ln.xPx != null ? (
<div
key={i}
className="helper-line helper-line-vertical"
style={{ left: ln.xPx + "px", top: 0 }}
/>
) : (
<div
key={i}
className="helper-line helper-line-horizontal"
style={{ top: ln.yPx + "px", left: 0 }}
/>
)
)}
{/* Node Context Menu */}
<Menu
open={Boolean(nodeContextMenu)}
onClose={() => setNodeContextMenu(null)}
anchorReference="anchorPosition"
anchorPosition={nodeContextMenu ? { top: nodeContextMenu.mouseY, left: nodeContextMenu.mouseX } : undefined}
PaperProps={{ sx: { bgcolor: "#1e1e1e", color: "#fff", fontSize: "13px" } }}
>
<MenuItem onClick={() => handleEditNodeProps(nodeContextMenu.nodeId)}>
<EditIcon sx={{ fontSize: 18, color: "#58a6ff", mr: 1 }} />
Edit Properties
</MenuItem>
<MenuItem onClick={() => handleDisconnectAllEdges(nodeContextMenu.nodeId)}>
<PolylineIcon sx={{ fontSize: 18, color: "#58a6ff", mr: 1 }} />
Disconnect All Edges
</MenuItem>
<MenuItem onClick={() => handleRemoveNode(nodeContextMenu.nodeId)}>
<DeleteForeverIcon sx={{ fontSize: 18, color: "#ff4f4f", mr: 1 }} />
Remove Node
</MenuItem>
</Menu>
{/* Edge Context Menu */}
<Menu
open={Boolean(edgeContextMenu)}
onClose={() => setEdgeContextMenu(null)}
anchorReference="anchorPosition"
anchorPosition={edgeContextMenu ? { top: edgeContextMenu.mouseY, left: edgeContextMenu.mouseX } : undefined}
PaperProps={{ sx: { bgcolor: "#1e1e1e", color: "#fff", fontSize: "13px" } }}
>
<MenuItem onClick={() => handleEditEdgeProps(edgeContextMenu.edgeId)}>
<EditIcon sx={{ fontSize: 18, color: "#58a6ff", mr: 1 }} />
Edit Properties
</MenuItem>
<MenuItem onClick={() => handleUnlinkEdge(edgeContextMenu.edgeId)}>
<DeleteForeverIcon sx={{ fontSize: 18, color: "#ff4f4f", mr: 1 }} />
Unlink Edge
</MenuItem>
</Menu>
</div>
);
}

View File

@@ -0,0 +1,100 @@
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/Flow_Tabs.jsx
import React from "react";
import { Box, Tabs, Tab, Tooltip } from "@mui/material";
import { Add as AddIcon } from "@mui/icons-material";
/**
* Renders the tab bar (including the "add tab" button).
*
* Props:
* - tabs (array of {id, tab_name, nodes, edges})
* - activeTabId (string)
* - onTabChange(newActiveTabId: string)
* - onAddTab()
* - onTabRightClick(evt: MouseEvent, tabId: string)
*/
export default function FlowTabs({
tabs,
activeTabId,
onTabChange,
onAddTab,
onTabRightClick
}) {
// Determine the currently active tab index
const activeIndex = (() => {
const idx = tabs.findIndex((t) => t.id === activeTabId);
return idx >= 0 ? idx : 0;
})();
// Handle tab clicks
const handleChange = (event, newValue) => {
if (newValue === "__addtab__") {
// The "plus" tab
onAddTab();
} else {
// normal tab index
const newTab = tabs[newValue];
if (newTab) {
onTabChange(newTab.id);
}
}
};
return (
<Box
sx={{
display: "flex",
alignItems: "center",
backgroundColor: "#232323",
borderBottom: "1px solid #333",
height: "36px"
}}
>
<Tabs
value={activeIndex}
onChange={handleChange}
variant="scrollable"
scrollButtons="auto"
textColor="inherit"
TabIndicatorProps={{
style: { backgroundColor: "#58a6ff" }
}}
sx={{
minHeight: "36px",
height: "36px",
flexGrow: 1
}}
>
{tabs.map((tab, index) => (
<Tab
key={tab.id}
label={tab.tab_name}
value={index}
onContextMenu={(evt) => onTabRightClick(evt, tab.id)}
sx={{
minHeight: "36px",
height: "36px",
textTransform: "none",
backgroundColor: tab.id === activeTabId ? "#2C2C2C" : "transparent",
color: "#58a6ff"
}}
/>
))}
{/* The "plus" tab has a special value */}
<Tooltip title="Create a New Concurrent Tab" arrow>
<Tab
icon={<AddIcon />}
value="__addtab__"
sx={{
minHeight: "36px",
height: "36px",
color: "#58a6ff",
textTransform: "none"
}}
/>
</Tooltip>
</Tabs>
</Box>
);
}

View File

@@ -0,0 +1,449 @@
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/Node_Configuration_Sidebar.jsx
import { Box, Typography, Tabs, Tab, TextField, MenuItem, IconButton, Dialog, DialogTitle, DialogContent, DialogActions, Button, Tooltip } from "@mui/material";
import React, { useState } from "react";
import { useReactFlow } from "reactflow";
import ReactMarkdown from "react-markdown"; // Used for Node Usage Documentation
import EditIcon from "@mui/icons-material/Edit";
import PaletteIcon from "@mui/icons-material/Palette";
import { SketchPicker } from "react-color";
// ---- NEW: Brightness utility for gradient ----
function darkenColor(hex, percent = 0.7) {
if (!/^#[0-9A-Fa-f]{6}$/.test(hex)) return hex;
let r = parseInt(hex.slice(1, 3), 16);
let g = parseInt(hex.slice(3, 5), 16);
let b = parseInt(hex.slice(5, 7), 16);
r = Math.round(r * percent);
g = Math.round(g * percent);
b = Math.round(b * percent);
return `#${r.toString(16).padStart(2,"0")}${g.toString(16).padStart(2,"0")}${b.toString(16).padStart(2,"0")}`;
}
// --------------------------------------------
export default function NodeConfigurationSidebar({ drawerOpen, setDrawerOpen, title, nodeData, setNodes, selectedNode }) {
const [activeTab, setActiveTab] = useState(0);
const contextSetNodes = useReactFlow().setNodes;
// Use setNodes from props if provided, else fallback to context (for backward compatibility)
const effectiveSetNodes = setNodes || contextSetNodes;
const handleTabChange = (_, newValue) => setActiveTab(newValue);
// Rename dialog state
const [renameOpen, setRenameOpen] = useState(false);
const [renameValue, setRenameValue] = useState(title || "");
// ---- NEW: Accent Color Picker ----
const [colorDialogOpen, setColorDialogOpen] = useState(false);
const accentColor = selectedNode?.data?.accentColor || "#58a6ff";
// ----------------------------------
const renderConfigFields = () => {
const config = nodeData?.config || [];
const nodeId = nodeData?.nodeId;
return config.map((field, index) => {
const value = nodeData?.[field.key] || "";
// ---- DYNAMIC DROPDOWN SUPPORT ----
if (field.type === "select") {
let options = field.options || [];
// Handle dynamic options for things like Target Window
if (field.dynamicOptions && nodeData?.windowList && Array.isArray(nodeData.windowList)) {
options = nodeData.windowList
.map(win => ({
value: String(win.handle),
label: `${win.title} (${win.handle})`
}))
.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
} else {
options = options.map(opt => ({ value: opt, label: opt }));
}
return (
<Box key={index} sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ color: "#ccc", mb: 0.5 }}>
{field.label || field.key}
</Typography>
<TextField
select
fullWidth
size="small"
value={value}
onChange={(e) => {
const newValue = e.target.value;
if (!nodeId) return;
effectiveSetNodes((nds) =>
nds.map((n) =>
n.id === nodeId
? { ...n, data: { ...n.data, [field.key]: newValue } }
: n
)
);
window.BorealisValueBus[nodeId] = newValue;
}}
SelectProps={{
MenuProps: {
PaperProps: {
sx: {
bgcolor: "#1e1e1e",
color: "#ccc",
border: "1px solid #58a6ff",
"& .MuiMenuItem-root": {
color: "#ccc",
fontSize: "0.85rem",
"&:hover": {
backgroundColor: "#2a2a2a"
},
"&.Mui-selected": {
backgroundColor: "#2c2c2c !important",
color: "#58a6ff"
},
"&.Mui-selected:hover": {
backgroundColor: "#2a2a2a !important"
}
}
}
}
}
}}
sx={{
"& .MuiOutlinedInput-root": {
backgroundColor: "#1e1e1e",
color: "#ccc",
fontSize: "0.85rem",
"& fieldset": {
borderColor: "#444"
},
"&:hover fieldset": {
borderColor: "#58a6ff"
},
"&.Mui-focused fieldset": {
borderColor: "#58a6ff"
}
},
"& .MuiSelect-select": {
backgroundColor: "#1e1e1e"
}
}}
>
{options.length === 0 ? (
<MenuItem disabled value="">
{field.label === "Target Window"
? "No windows detected"
: "No options"}
</MenuItem>
) : (
options.map((opt, idx) => (
<MenuItem key={idx} value={opt.value}>
{opt.label}
</MenuItem>
))
)}
</TextField>
</Box>
);
}
// ---- END DYNAMIC DROPDOWN SUPPORT ----
return (
<Box key={index} sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ color: "#ccc", mb: 0.5 }}>
{field.label || field.key}
</Typography>
<TextField
variant="outlined"
size="small"
fullWidth
value={value}
onChange={(e) => {
const newValue = e.target.value;
if (!nodeId) return;
effectiveSetNodes((nds) =>
nds.map((n) =>
n.id === nodeId
? { ...n, data: { ...n.data, [field.key]: newValue } }
: n
)
);
window.BorealisValueBus[nodeId] = newValue;
}}
InputProps={{
sx: {
backgroundColor: "#1e1e1e",
color: "#ccc",
"& fieldset": { borderColor: "#444" },
"&:hover fieldset": { borderColor: "#666" },
"&.Mui-focused fieldset": { borderColor: "#58a6ff" }
}
}}
/>
</Box>
);
});
};
// ---- NEW: Accent Color Button ----
const renderAccentColorButton = () => (
<Tooltip title="Override Node Header/Accent Color">
<IconButton
size="small"
aria-label="Override Node Color"
onClick={() => setColorDialogOpen(true)}
sx={{
ml: 1,
border: "1px solid #58a6ff",
background: accentColor,
color: "#222",
width: 28, height: 28, p: 0
}}
>
<PaletteIcon fontSize="small" />
</IconButton>
</Tooltip>
);
// ----------------------------------
return (
<>
<Box
onClick={() => setDrawerOpen(false)}
sx={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(0, 0, 0, 0.3)",
opacity: drawerOpen ? 1 : 0,
pointerEvents: drawerOpen ? "auto" : "none",
transition: "opacity 0.6s ease",
zIndex: 10
}}
/>
<Box
sx={{
position: "absolute",
top: 0,
right: 0,
bottom: 0,
width: 400,
bgcolor: "#2C2C2C",
color: "#ccc",
borderLeft: "1px solid #333",
padding: 0,
zIndex: 11,
overflowY: "auto",
transform: drawerOpen ? "translateX(0)" : "translateX(100%)",
transition: "transform 0.3s ease"
}}
onClick={(e) => e.stopPropagation()}
>
<Box sx={{ backgroundColor: "#232323", borderBottom: "1px solid #333" }}>
<Box sx={{ padding: "12px 16px" }}>
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<Typography variant="h7" sx={{ color: "#0475c2", fontWeight: "bold" }}>
{"Edit " + (title || "Node")}
</Typography>
<Box sx={{ display: "flex", alignItems: "center" }}>
<IconButton
size="small"
aria-label="Rename Node"
onClick={() => {
setRenameValue(title || "");
setRenameOpen(true);
}}
sx={{ ml: 1, color: "#58a6ff" }}
>
<EditIcon fontSize="small" />
</IconButton>
{/* ---- NEW: Accent Color Picker button next to pencil ---- */}
{renderAccentColorButton()}
{/* ------------------------------------------------------ */}
</Box>
</Box>
</Box>
<Tabs
value={activeTab}
onChange={handleTabChange}
variant="fullWidth"
textColor="inherit"
TabIndicatorProps={{ style: { backgroundColor: "#ccc" } }}
sx={{
borderTop: "1px solid #333",
borderBottom: "1px solid #333",
minHeight: "36px",
height: "36px"
}}
>
<Tab
label="Config"
sx={{
color: "#ccc",
"&.Mui-selected": { color: "#ccc" },
minHeight: "36px",
height: "36px",
textTransform: "none"
}}
/>
<Tab
label="Usage Docs"
sx={{
color: "#ccc",
"&.Mui-selected": { color: "#ccc" },
minHeight: "36px",
height: "36px",
textTransform: "none"
}}
/>
</Tabs>
</Box>
<Box sx={{ padding: 2 }}>
{activeTab === 0 && renderConfigFields()}
{activeTab === 1 && (
<Box sx={{ fontSize: "0.85rem", color: "#aaa" }}>
<ReactMarkdown
children={nodeData?.usage_documentation || "No usage documentation provided for this node."}
components={{
h3: ({ node, ...props }) => (
<Typography variant="h6" sx={{ color: "#58a6ff", mb: 1 }} {...props} />
),
p: ({ node, ...props }) => (
<Typography paragraph sx={{ mb: 1.5 }} {...props} />
),
ul: ({ node, ...props }) => (
<ul style={{ marginBottom: "1em", paddingLeft: "1.2em" }} {...props} />
),
li: ({ node, ...props }) => (
<li style={{ marginBottom: "0.5em" }} {...props} />
)
}}
/>
</Box>
)}
</Box>
</Box>
{/* Rename Node Dialog */}
<Dialog
open={renameOpen}
onClose={() => setRenameOpen(false)}
PaperProps={{ sx: { bgcolor: "#232323" } }}
>
<DialogTitle>Rename Node</DialogTitle>
<DialogContent>
<TextField
autoFocus
fullWidth
variant="outlined"
label="Node Title"
value={renameValue}
onChange={(e) => setRenameValue(e.target.value)}
sx={{
mt: 1,
bgcolor: "#1e1e1e",
"& .MuiOutlinedInput-root": {
color: "#ccc",
backgroundColor: "#1e1e1e",
"& fieldset": { borderColor: "#444" }
},
label: { color: "#aaa" }
}}
/>
</DialogContent>
<DialogActions>
<Button sx={{ color: "#aaa" }} onClick={() => setRenameOpen(false)}>
Cancel
</Button>
<Button
sx={{ color: "#58a6ff" }}
onClick={() => {
// Use selectedNode (passed as prop) or nodeData?.nodeId as fallback
const nodeId = selectedNode?.id || nodeData?.nodeId;
if (!nodeId) {
setRenameOpen(false);
return;
}
effectiveSetNodes((nds) =>
nds.map((n) =>
n.id === nodeId
? { ...n, data: { ...n.data, label: renameValue } }
: n
)
);
setRenameOpen(false);
}}
>
Save
</Button>
</DialogActions>
</Dialog>
{/* ---- NEW: Accent Color Picker Dialog ---- */}
<Dialog
open={colorDialogOpen}
onClose={() => setColorDialogOpen(false)}
PaperProps={{ sx: { bgcolor: "#232323" } }}
>
<DialogTitle>Pick Node Header/Accent Color</DialogTitle>
<DialogContent>
<SketchPicker
color={accentColor}
onChangeComplete={(color) => {
const nodeId = selectedNode?.id || nodeData?.nodeId;
if (!nodeId) return;
const accent = color.hex;
const accentDark = darkenColor(accent, 0.7);
effectiveSetNodes((nds) =>
nds.map((n) =>
n.id === nodeId
? {
...n,
data: { ...n.data, accentColor: accent },
style: {
...n.style,
"--borealis-accent": accent,
"--borealis-accent-dark": accentDark,
"--borealis-title": accent,
},
}
: n
)
);
}}
disableAlpha
presetColors={[
"#58a6ff", "#0475c2", "#00d18c", "#ff4f4f", "#ff8c00",
"#6b21a8", "#0e7490", "#888", "#fff", "#000"
]}
/>
<Box sx={{ mt: 2 }}>
<Typography variant="body2">
The node's header text and accent gradient will use your selected color.<br />
The accent gradient fades to a slightly darker version.
</Typography>
<Box sx={{ mt: 2, display: "flex", alignItems: "center" }}>
<span style={{
display: "inline-block",
width: 48,
height: 22,
borderRadius: 4,
border: "1px solid #888",
background: `linear-gradient(to bottom, ${accentColor} 0%, ${darkenColor(accentColor, 0.7)} 100%)`
}} />
<span style={{ marginLeft: 10, color: accentColor, fontWeight: "bold" }}>
{accentColor}
</span>
</Box>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setColorDialogOpen(false)} sx={{ color: "#aaa" }}>Close</Button>
</DialogActions>
</Dialog>
{/* ---- END ACCENT COLOR PICKER DIALOG ---- */}
</>
);
}

View File

@@ -0,0 +1,260 @@
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/Node_Sidebar.jsx
import React, { useState } from "react";
import {
Accordion,
AccordionSummary,
AccordionDetails,
Button,
Tooltip,
Typography,
Box
} from "@mui/material";
import {
ExpandMore as ExpandMoreIcon,
SaveAlt as SaveAltIcon,
Save as SaveIcon,
FileOpen as FileOpenIcon,
DeleteForever as DeleteForeverIcon,
DragIndicator as DragIndicatorIcon,
Polyline as PolylineIcon,
ChevronLeft as ChevronLeftIcon,
ChevronRight as ChevronRightIcon
} from "@mui/icons-material";
import { SaveWorkflowDialog } from "../Dialogs";
export default function NodeSidebar({
categorizedNodes,
handleExportFlow,
handleImportFlow,
handleSaveFlow,
handleOpenCloseAllDialog,
fileInputRef,
onFileInputChange,
currentTabName
}) {
const [expandedCategory, setExpandedCategory] = useState(null);
const [collapsed, setCollapsed] = useState(false);
const [saveOpen, setSaveOpen] = useState(false);
const [saveName, setSaveName] = useState("");
const handleAccordionChange = (category) => (_, isExpanded) => {
setExpandedCategory(isExpanded ? category : null);
};
return (
<div
style={{
width: collapsed ? 40 : 300,
backgroundColor: "#121212",
borderRight: "1px solid #333",
overflow: "hidden",
display: "flex",
flexDirection: "column",
height: "100%"
}}
>
<div style={{ flex: 1, overflowY: "auto" }}>
{!collapsed && (
<>
{/* Workflows Section */}
<Accordion
defaultExpanded
square
disableGutters
sx={{ "&:before": { display: "none" }, margin: 0, border: 0 }}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
sx={{
backgroundColor: "#2c2c2c",
minHeight: "36px",
"& .MuiAccordionSummary-content": { margin: 0 }
}}
>
<Typography sx={{ fontSize: "0.9rem", color: "#0475c2" }}>
<b>Workflows</b>
</Typography>
</AccordionSummary>
<AccordionDetails sx={{ p: 0, bgcolor: "#232323" }}>
<Tooltip title="Save Current Flow to Workflows Folder" placement="right" arrow>
<Button
fullWidth
startIcon={<SaveIcon />}
onClick={() => {
setSaveName(currentTabName || "workflow");
setSaveOpen(true);
}}
sx={buttonStyle}
>
Save Workflow
</Button>
</Tooltip>
<Tooltip title="Import JSON File into New Flow Tab" placement="right" arrow>
<Button fullWidth startIcon={<FileOpenIcon />} onClick={handleImportFlow} sx={buttonStyle}>
Import Workflow (JSON)
</Button>
</Tooltip>
<Tooltip title="Export Current Tab to a JSON File" placement="right" arrow>
<Button fullWidth startIcon={<SaveAltIcon />} onClick={handleExportFlow} sx={buttonStyle}>
Export Workflow (JSON)
</Button>
</Tooltip>
</AccordionDetails>
</Accordion>
{/* Nodes Section */}
<Accordion
defaultExpanded
square
disableGutters
sx={{ "&:before": { display: "none" }, margin: 0, border: 0 }}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
sx={{
backgroundColor: "#2c2c2c",
minHeight: "36px",
"& .MuiAccordionSummary-content": { margin: 0 }
}}
>
<Typography sx={{ fontSize: "0.9rem", color: "#0475c2" }}>
<b>Nodes</b>
</Typography>
</AccordionSummary>
<AccordionDetails sx={{ p: 0 }}>
{Object.entries(categorizedNodes).map(([category, items]) => (
<Accordion
key={category}
square
expanded={expandedCategory === category}
onChange={handleAccordionChange(category)}
disableGutters
sx={{
bgcolor: "#232323",
"&:before": { display: "none" },
margin: 0,
border: 0
}}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
sx={{
bgcolor: "#1e1e1e",
px: 2,
minHeight: "32px",
"& .MuiAccordionSummary-content": { margin: 0 }
}}
>
<Typography sx={{ color: "#888", fontSize: "0.75rem" }}>
{category}
</Typography>
</AccordionSummary>
<AccordionDetails sx={{ px: 1, py: 0 }}>
{items.map((nodeDef) => (
<Tooltip
key={`${category}-${nodeDef.type}`}
title={
<span style={{ whiteSpace: "pre-line", wordWrap: "break-word", maxWidth: 220 }}>
{nodeDef.description || "Drag & Drop into Editor"}
</span>
}
placement="right"
arrow
>
<Button
fullWidth
sx={nodeButtonStyle}
draggable
onDragStart={(event) => {
event.dataTransfer.setData("application/reactflow", nodeDef.type);
event.dataTransfer.effectAllowed = "move";
}}
startIcon={<DragIndicatorIcon sx={{ color: "#666", fontSize: 18 }} />}
>
<span style={{ flexGrow: 1, textAlign: "left" }}>{nodeDef.label}</span>
<PolylineIcon sx={{ color: "#58a6ff", fontSize: 18, ml: 1 }} />
</Button>
</Tooltip>
))}
</AccordionDetails>
</Accordion>
))}
</AccordionDetails>
</Accordion>
{/* Hidden file input */}
<input
type="file"
accept=".json,application/json"
style={{ display: "none" }}
ref={fileInputRef}
onChange={onFileInputChange}
/>
</>
)}
</div>
{/* Bottom toggle button */}
<Tooltip title={collapsed ? "Expand Sidebar" : "Collapse Sidebar"} placement="left">
<Box
onClick={() => setCollapsed(!collapsed)}
sx={{
height: "36px",
borderTop: "1px solid #333",
cursor: "pointer",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "#888",
backgroundColor: "#121212",
transition: "background-color 0.2s ease",
"&:hover": {
backgroundColor: "#1e1e1e"
},
"&:active": {
backgroundColor: "#2a2a2a"
}
}}
>
{collapsed ? <ChevronRightIcon /> : <ChevronLeftIcon />}
</Box>
</Tooltip>
<SaveWorkflowDialog
open={saveOpen}
value={saveName}
onChange={setSaveName}
onCancel={() => setSaveOpen(false)}
onSave={() => {
setSaveOpen(false);
handleSaveFlow(saveName);
}}
/>
</div>
);
}
const buttonStyle = {
color: "#ccc",
backgroundColor: "#232323",
justifyContent: "flex-start",
pl: 2,
fontSize: "0.9rem",
textTransform: "none",
"&:hover": {
backgroundColor: "#2a2a2a"
}
};
const nodeButtonStyle = {
color: "#ccc",
backgroundColor: "#232323",
justifyContent: "space-between",
pl: 2,
pr: 1,
fontSize: "0.9rem",
textTransform: "none",
"&:hover": {
backgroundColor: "#2a2a2a"
}
};