Browse Source

fix: anchor group label above group so it grows upward and use useViewport

Restructure floating group label to use bottom-anchored positioning so
inverse-zoom scaling grows upward instead of overlapping group content.
Switch from useReactFlow().getViewport() to useViewport() hook for
proper reactive zoom updates. Change group name to double-click to edit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 4 months ago
parent
commit
42ed1598bf
  1. 30
      src/components/GroupsOverlay.tsx

30
src/components/GroupsOverlay.tsx

@ -1,7 +1,7 @@
"use client"; "use client";
import { useCallback, useState, useRef, useEffect } from "react"; import { useCallback, useState, useRef, useEffect } from "react";
import { useReactFlow, ViewportPortal } from "@xyflow/react"; import { useViewport, 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";
@ -280,17 +280,24 @@ function GroupControls({ groupId, zoom }: GroupControlsProps) {
}} }}
> >
{/* Floating group name label - top-left, viewport-scaled */} {/* Floating group name label - top-left, viewport-scaled */}
{/* Outer wrapper: zero-height anchor at the top edge of the group */}
<div
className="absolute left-0"
style={{ top: 0, height: 0, overflow: "visible" }}
>
{/* Inner scaled element: bottom-anchored so it grows upward, scale keeps bottom-left fixed */}
<div <div
className="absolute left-0 pointer-events-auto cursor-grab active:cursor-grabbing select-none" className="absolute left-0 pointer-events-auto cursor-grab active:cursor-grabbing select-none"
style={{ style={{
top: -2, bottom: 0,
transform: `scale(${1 / zoom})`, transform: `scale(${1 / zoom})`,
transformOrigin: "bottom left", transformOrigin: "bottom left",
whiteSpace: "nowrap",
}} }}
onMouseDown={handleHeaderMouseDown} onMouseDown={handleHeaderMouseDown}
> >
<div <div
className="flex items-center rounded-md px-2 py-0.5" className="flex items-center rounded-md px-2 py-0.5 mb-1"
style={{ backgroundColor: bgColor }} style={{ backgroundColor: bgColor }}
> >
{isEditing ? ( {isEditing ? (
@ -306,26 +313,25 @@ function GroupControls({ groupId, zoom }: GroupControlsProps) {
/> />
) : ( ) : (
<span <span
className="text-xs font-medium text-white truncate cursor-text" className="text-xs font-medium text-white truncate"
style={{ maxWidth: 200 }} style={{ maxWidth: 200 }}
onClick={() => setIsEditing(true)} onDoubleClick={(e) => { e.stopPropagation(); setIsEditing(true); }}
> >
{group.name} {group.name}
</span> </span>
)} )}
</div> </div>
</div> </div>
</div>
{/* Floating controls - top-right, viewport-scaled */} {/* Floating controls - top-right, scales naturally with canvas zoom */}
<div <div
className="absolute right-0 pointer-events-auto" className="absolute right-0 pointer-events-auto"
style={{ style={{
top: -2, top: -28,
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"> <div className="flex items-center gap-1 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
@ -486,7 +492,7 @@ export function GroupBackgroundsPortal() {
// Renders group controls (headers, resize handles) using ViewportPortal above nodes // Renders group controls (headers, resize handles) using ViewportPortal above nodes
export function GroupControlsOverlay() { export function GroupControlsOverlay() {
const { groups } = useWorkflowStore(); const { groups } = useWorkflowStore();
const viewport = useReactFlow().getViewport(); const { zoom } = useViewport();
const groupIds = Object.keys(groups); const groupIds = Object.keys(groups);
@ -496,7 +502,7 @@ export function GroupControlsOverlay() {
<ViewportPortal> <ViewportPortal>
<div style={{ position: "absolute", top: 0, left: 0, zIndex: 1000, pointerEvents: "none" }}> <div style={{ position: "absolute", top: 0, left: 0, zIndex: 1000, pointerEvents: "none" }}>
{groupIds.map((groupId) => ( {groupIds.map((groupId) => (
<GroupControls key={groupId} groupId={groupId} zoom={viewport.zoom} /> <GroupControls key={groupId} groupId={groupId} zoom={zoom} />
))} ))}
</div> </div>
</ViewportPortal> </ViewportPortal>

Loading…
Cancel
Save