diff --git a/skyvern-frontend/src/components/RadialMenu.tsx b/skyvern-frontend/src/components/RadialMenu.tsx index ed266a8d..b5c00cf0 100644 --- a/skyvern-frontend/src/components/RadialMenu.tsx +++ b/skyvern-frontend/src/components/RadialMenu.tsx @@ -18,8 +18,8 @@ interface RadialMenuProps { */ buttonSize?: string; /** - * The gap between items in degrees. If not provided, items are evenly spaced - * around the circle. + * The gap between items in degrees (for radial layout) or pixels (for vertical layout). + * If not provided, items are evenly spaced around the circle (radial) or use 8px gap (vertical). */ gap?: number; /** @@ -35,6 +35,11 @@ interface RadialMenuProps { * If true, rotates the text so its baseline runs parallel to the radial line. */ rotateText?: boolean; + /** + * Layout mode: "radial" (circular) or "vertical" (stacked list to the right). + * Defaults to "radial". + */ + layout?: "radial" | "vertical"; } const proportionalAngle = ( @@ -76,6 +81,7 @@ export function RadialMenu({ gap, startAt, rotateText, + layout = "radial", }: RadialMenuProps) { const [isOpen, setIsOpen] = useState(false); const [calculatedRadius, setCalculatedRadius] = useState(100); @@ -137,13 +143,72 @@ export function RadialMenu({ {visibleItems.map((item, index) => { + const isEnabled = item.enabled !== false; + const btnSize = buttonSize ?? "40px"; + const btnSizeNum = parseFloat(btnSize); + + if (layout === "vertical") { + // Vertical layout: stack items to the right of center + const verticalGap = gap ?? 8; + const xOffset = numRadius; + // Center the stack vertically around the middle item + const totalHeight = + visibleItems.length * btnSizeNum + + (visibleItems.length - 1) * verticalGap; + const startY = -totalHeight / 2 + btnSizeNum / 2; + const yOffset = startY + index * (btnSizeNum + verticalGap); + + return ( +
{ + if (isEnabled) { + item.onClick(); + setIsOpen(false); + } + }} + className="absolute left-1/2 top-1/2 z-30 flex cursor-pointer items-center gap-2 transition-all duration-300 ease-out" + style={{ + transform: isOpen + ? `translate(0%, -50%) translate(${xOffset}px, ${yOffset}px) scale(1)` + : `translate(0%, -50%) translate(0, 0) scale(0)`, + opacity: isOpen ? (isEnabled ? 1 : 0.5) : 0, + pointerEvents: isOpen ? (isEnabled ? "auto" : "none") : "none", + transitionDelay: isOpen ? `${index * 50}ms` : "0ms", + }} + > + + {item.text && ( + + {item.text} + + )} +
+ ); + } + + // Radial layout (default) const angle = gap !== undefined ? gappedAngle(index, gap, startAt) : proportionalAngle(index, visibleItems.length, startAt); const x = Math.cos(angle) * parseFloat(radiusValue); const y = Math.sin(angle) * parseFloat(radiusValue); - const isEnabled = item.enabled !== false; // calculate text offset along the radial line const textDistance = 0.375 * numRadius; @@ -170,8 +235,8 @@ export function RadialMenu({ disabled={!isEnabled} className="absolute left-1/2 top-1/2 z-30 flex items-center justify-center rounded-full bg-white shadow-lg transition-all duration-300 ease-out hover:bg-gray-50 disabled:cursor-not-allowed" style={{ - width: buttonSize ?? "40px", - height: buttonSize ?? "40px", + width: btnSize, + height: btnSize, transform: isOpen ? `translate(-50%, -50%) translate(${x}px, ${y}px) scale(1)` : "translate(-50%, -50%) translate(0, 0) scale(0)", diff --git a/skyvern-frontend/src/routes/workflows/editor/WorkflowAddMenu.tsx b/skyvern-frontend/src/routes/workflows/editor/WorkflowAddMenu.tsx index 2199575e..9124cfea 100644 --- a/skyvern-frontend/src/routes/workflows/editor/WorkflowAddMenu.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/WorkflowAddMenu.tsx @@ -97,6 +97,7 @@ function WorkflowAddMenu({ startAt={startAt} gap={gap} rotateText={rotateText} + layout="vertical" > {children}