Browse Source

feat(quick-21): replace group header bar with floating viewport-scaled label and controls

- Remove solid color header bar from groups, replace with floating rounded-rectangle label at top-left
- Add floating controls toolbar (color picker, lock, delete) at top-right with bg-neutral-800/80 backdrop
- Both label and controls use transform: scale(1/zoom) for constant screen size regardless of canvas zoom
- Remove headerHeight offset from createGroup in workflowStore (no longer reserves vertical space for header)
- Keep all existing group interactions: rename, recolor, lock, delete, drag, resize

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 4 months ago
parent
commit
0c0994e97c
  1. 47
      src/components/GroupsOverlay.tsx
  2. 5
      src/store/workflowStore.ts

47
src/components/GroupsOverlay.tsx

@ -5,8 +5,6 @@ import { useReactFlow, ViewportPortal } from "@xyflow/react";
import { useWorkflowStore, GROUP_COLORS } from "@/store/workflowStore"; import { useWorkflowStore, GROUP_COLORS } from "@/store/workflowStore";
import { GroupColor } from "@/types"; import { GroupColor } from "@/types";
const HEADER_HEIGHT = 32;
const COLOR_OPTIONS: { color: GroupColor; label: string }[] = [ const COLOR_OPTIONS: { color: GroupColor; label: string }[] = [
{ color: "neutral", label: "Gray" }, { color: "neutral", label: "Gray" },
{ color: "blue", label: "Blue" }, { color: "blue", label: "Blue" },
@ -278,13 +276,22 @@ function GroupControls({ groupId, zoom }: GroupControlsProps) {
width: group.size.width, width: group.size.width,
height: group.size.height, height: group.size.height,
pointerEvents: "none", pointerEvents: "none",
overflow: "visible",
}} }}
> >
{/* Header - interactive */} {/* Floating group name label - top-left, viewport-scaled */}
<div <div
className="absolute top-0 left-0 right-0 flex items-center gap-2 px-3 cursor-grab active:cursor-grabbing select-none rounded-t-xl pointer-events-auto" className="absolute left-0 pointer-events-auto cursor-grab active:cursor-grabbing select-none"
style={{ backgroundColor: bgColor, height: HEADER_HEIGHT }} style={{
top: -2,
transform: `scale(${1 / zoom})`,
transformOrigin: "bottom left",
}}
onMouseDown={handleHeaderMouseDown} onMouseDown={handleHeaderMouseDown}
>
<div
className="flex items-center rounded-md px-2 py-0.5"
style={{ backgroundColor: bgColor }}
> >
{isEditing ? ( {isEditing ? (
<input <input
@ -294,27 +301,36 @@ function GroupControls({ groupId, zoom }: GroupControlsProps) {
onChange={(e) => setEditName(e.target.value)} onChange={(e) => setEditName(e.target.value)}
onBlur={handleNameSubmit} onBlur={handleNameSubmit}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
className="bg-transparent border-none outline-none text-sm font-medium text-white px-0 py-0" className="bg-transparent border-none outline-none text-xs font-medium text-white px-0 py-0"
style={{ minWidth: 60, maxWidth: 200, width: `${Math.max(60, editName.length * 8)}px` }} style={{ minWidth: 60, maxWidth: 200, width: `${Math.max(60, editName.length * 7)}px` }}
/> />
) : ( ) : (
<span <span
className="text-sm font-medium text-white truncate cursor-text" className="text-xs font-medium text-white truncate cursor-text"
style={{ maxWidth: 200 }} style={{ maxWidth: 200 }}
onClick={() => setIsEditing(true)} onClick={() => setIsEditing(true)}
> >
{group.name} {group.name}
</span> </span>
)} )}
</div>
</div>
{/* Spacer for drag area */} {/* Floating controls - top-right, viewport-scaled */}
<div className="flex-1" /> <div
className="absolute right-0 pointer-events-auto"
style={{
top: -2,
transform: `scale(${1 / zoom})`,
transformOrigin: "bottom right",
}}
>
<div className="flex items-center gap-1 rounded-md bg-neutral-800/80 backdrop-blur-sm px-1.5 py-0.5">
{/* Color Picker */} {/* Color Picker */}
<div className="relative flex items-center" ref={colorPickerRef}> <div className="relative flex items-center" ref={colorPickerRef}>
<button <button
onClick={() => setShowColorPicker(!showColorPicker)} onClick={() => setShowColorPicker(!showColorPicker)}
className="w-5 h-5 rounded border border-white/30 hover:border-white/60 transition-colors" className="w-4 h-4 rounded border border-white/30 hover:border-white/60 transition-colors"
style={{ backgroundColor: bgColor }} style={{ backgroundColor: bgColor }}
title="Change color" title="Change color"
/> />
@ -388,11 +404,11 @@ function GroupControls({ groupId, zoom }: GroupControlsProps) {
title={group.locked ? "Unlock group" : "Lock group"} title={group.locked ? "Unlock group" : "Lock group"}
> >
{group.locked ? ( {group.locked ? (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg> </svg>
) : ( ) : (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8 11V7a4 4 0 118 0m-4 8v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M8 11V7a4 4 0 118 0m-4 8v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z" />
</svg> </svg>
)} )}
@ -404,11 +420,12 @@ function GroupControls({ groupId, zoom }: GroupControlsProps) {
className="p-0.5 rounded hover:bg-white/20 text-white/70 hover:text-white transition-colors" className="p-0.5 rounded hover:bg-white/20 text-white/70 hover:text-white transition-colors"
title="Delete group" title="Delete group"
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" /> <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
</button> </button>
</div> </div>
</div>
{/* Resize handles - interactive */} {/* Resize handles - interactive */}
<div <div

5
src/store/workflowStore.ts

@ -813,7 +813,6 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({
// Add padding around nodes // Add padding around nodes
const padding = 20; const padding = 20;
const headerHeight = 32; // Match HEADER_HEIGHT in GroupsOverlay
// Find next available color // Find next available color
const usedColors = new Set(Object.values(groups).map((g) => g.color)); const usedColors = new Set(Object.values(groups).map((g) => g.color));
@ -836,11 +835,11 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({
color, color,
position: { position: {
x: minX - padding, x: minX - padding,
y: minY - padding - headerHeight y: minY - padding,
}, },
size: { size: {
width: maxX - minX + padding * 2, width: maxX - minX + padding * 2,
height: maxY - minY + padding * 2 + headerHeight, height: maxY - minY + padding * 2,
}, },
}; };

Loading…
Cancel
Save