diff --git a/src/components/GroupsOverlay.tsx b/src/components/GroupsOverlay.tsx
index 4da29c40..1e9e9a1a 100644
--- a/src/components/GroupsOverlay.tsx
+++ b/src/components/GroupsOverlay.tsx
@@ -5,8 +5,6 @@ import { useReactFlow, ViewportPortal } from "@xyflow/react";
import { useWorkflowStore, GROUP_COLORS } from "@/store/workflowStore";
import { GroupColor } from "@/types";
-const HEADER_HEIGHT = 32;
-
const COLOR_OPTIONS: { color: GroupColor; label: string }[] = [
{ color: "neutral", label: "Gray" },
{ color: "blue", label: "Blue" },
@@ -278,136 +276,155 @@ function GroupControls({ groupId, zoom }: GroupControlsProps) {
width: group.size.width,
height: group.size.height,
pointerEvents: "none",
+ overflow: "visible",
}}
>
- {/* Header - interactive */}
+ {/* Floating group name label - top-left, viewport-scaled */}
- {isEditing ? (
-
setEditName(e.target.value)}
- onBlur={handleNameSubmit}
- onKeyDown={handleKeyDown}
- className="bg-transparent border-none outline-none text-sm font-medium text-white px-0 py-0"
- style={{ minWidth: 60, maxWidth: 200, width: `${Math.max(60, editName.length * 8)}px` }}
- />
- ) : (
-
setIsEditing(true)}
- >
- {group.name}
-
- )}
-
- {/* Spacer for drag area */}
-
-
- {/* Color Picker */}
-
-
+
- {/* Delete Button */}
-
-
-
+ {/* Floating controls - top-right, viewport-scaled */}
+
+
+ {/* Color Picker */}
+
+
setShowColorPicker(!showColorPicker)}
+ className="w-4 h-4 rounded border border-white/30 hover:border-white/60 transition-colors"
+ style={{ backgroundColor: bgColor }}
+ title="Change color"
+ />
+ {showColorPicker && (
+ <>
+ {/* Invisible backdrop to catch clicks outside */}
+ setShowColorPicker(false)}
+ />
+
+ {COLOR_OPTIONS.map(({ color, label }, index) => {
+ // Fan out in an arc above the button
+ const totalItems = COLOR_OPTIONS.length;
+ const arcSpread = 180; // degrees of arc spread (wider)
+ const startAngle = -90 - arcSpread / 2; // start from top-left
+ const angleStep = arcSpread / (totalItems - 1);
+ const angle = startAngle + index * angleStep;
+ const radius = 55; // distance from center (larger)
+ const rad = (angle * Math.PI) / 180;
+ const x = Math.cos(rad) * radius;
+ const y = Math.sin(rad) * radius;
+ const finalX = x - 12;
+ const finalY = y - 12;
+
+ return (
+ handleColorChange(color)}
+ className={`absolute w-6 h-6 rounded-full border-2 transition-[transform,border-color] duration-150 hover:scale-110 ${
+ group.color === color
+ ? "border-white"
+ : "border-transparent hover:border-white/50"
+ }`}
+ style={{
+ backgroundColor: PICKER_PREVIEW_COLORS[color],
+ transform: `translate(${finalX}px, ${finalY}px)`,
+ animation: `colorFanIn-${index} 0.25s cubic-bezier(0.34, 1.56, 0.64, 1) forwards`,
+ animationDelay: `${index * 0.025}s`,
+ opacity: 0,
+ // Use CSS custom properties to pass the final position to the animation
+ ["--final-x" as string]: `${finalX}px`,
+ ["--final-y" as string]: `${finalY}px`,
+ }}
+ title={label}
+ >
+
+
+ );
+ })}
+
+ >
+ )}
+
+
+ {/* Lock/Unlock Button */}
+
+ {group.locked ? (
+
+ ) : (
+
+ )}
+
+
+ {/* Delete Button */}
+
+
+
+
{/* Resize handles - interactive */}
diff --git a/src/store/workflowStore.ts b/src/store/workflowStore.ts
index 731ef35b..472206ab 100644
--- a/src/store/workflowStore.ts
+++ b/src/store/workflowStore.ts
@@ -813,7 +813,6 @@ const workflowStoreImpl: StateCreator
= (set, get) => ({
// Add padding around nodes
const padding = 20;
- const headerHeight = 32; // Match HEADER_HEIGHT in GroupsOverlay
// Find next available color
const usedColors = new Set(Object.values(groups).map((g) => g.color));
@@ -836,11 +835,11 @@ const workflowStoreImpl: StateCreator = (set, get) => ({
color,
position: {
x: minX - padding,
- y: minY - padding - headerHeight
+ y: minY - padding,
},
size: {
width: maxX - minX + padding * 2,
- height: maxY - minY + padding * 2 + headerHeight,
+ height: maxY - minY + padding * 2,
},
};