Browse Source

feat: redesign group menu as vertical context menu

Replace inline horizontal controls with a vertical dropdown menu that
appears above the three-dot button. Menu has icon+label rows for
Background (color picker), Lock/Unlock, and Delete. Color picker fan
anchors from top-left of the menu. Three-dot button is always visible.

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

65
src/components/GroupsOverlay.tsx

@ -332,32 +332,43 @@ function GroupControls({ groupId, zoom }: GroupControlsProps) {
)} )}
</div> </div>
{/* Expanded controls - shown when menu is open */} {/* Three-dot menu toggle - always visible */}
<div className="relative">
<button
onClick={(e) => { e.stopPropagation(); setShowMenu(!showMenu); }}
className="w-6 h-6 rounded-md flex flex-col items-center justify-center gap-[2px] hover:bg-white/20 transition-colors"
title="Group options"
>
<div className="w-[3px] h-[3px] rounded-full bg-white/70" />
<div className="w-[3px] h-[3px] rounded-full bg-white/70" />
<div className="w-[3px] h-[3px] rounded-full bg-white/70" />
</button>
{/* Vertical context menu - appears above the three-dot button */}
{showMenu && ( {showMenu && (
<div className="flex items-center gap-px ml-1"> <div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-1 bg-neutral-800/90 backdrop-blur rounded-lg py-1 min-w-[130px] shadow-lg shadow-black/30">
{/* Color Picker */} {/* Background color row */}
<div className="relative flex items-center" ref={colorPickerRef}> <div className="relative" ref={colorPickerRef}>
<button <button
onClick={(e) => { e.stopPropagation(); setShowColorPicker(!showColorPicker); }} onClick={(e) => { e.stopPropagation(); setShowColorPicker(!showColorPicker); }}
className="w-6 h-6 rounded-md flex items-center justify-center hover:bg-white/20 transition-colors" className="flex items-center gap-2 px-3 py-1.5 w-full hover:bg-white/10 text-xs text-white/80 transition-colors"
title="Change color"
> >
<div <div
className="w-3.5 h-3.5 rounded border border-white/30 hover:border-white/60 transition-colors" className="w-3 h-3 rounded-full border border-white/30"
style={{ backgroundColor: bgColor }} style={{ backgroundColor: bgColor }}
/> />
<span>Background</span>
</button> </button>
{showColorPicker && ( {showColorPicker && (
<> <>
{/* Invisible backdrop to catch clicks outside */}
<div <div
className="fixed inset-0 z-40" className="fixed inset-0 z-40"
onClick={() => setShowColorPicker(false)} onClick={() => setShowColorPicker(false)}
/> />
<div className="absolute bottom-full left-1/2 mb-2 z-50 pointer-events-auto" style={{ transform: "translateX(-50%)" }}> <div className="absolute bottom-full left-0 mb-1 z-50 pointer-events-auto">
{COLOR_OPTIONS.map(({ color, label }, index) => { {COLOR_OPTIONS.map(({ color, label }, index) => {
const totalItems = COLOR_OPTIONS.length; const totalItems = COLOR_OPTIONS.length;
const arcSpread = 180; const arcSpread = 150;
const startAngle = -90 - arcSpread / 2; const startAngle = -90 - arcSpread / 2;
const angleStep = arcSpread / (totalItems - 1); const angleStep = arcSpread / (totalItems - 1);
const angle = startAngle + index * angleStep; const angle = startAngle + index * angleStep;
@ -365,7 +376,7 @@ function GroupControls({ groupId, zoom }: GroupControlsProps) {
const rad = (angle * Math.PI) / 180; const rad = (angle * Math.PI) / 180;
const x = Math.cos(rad) * radius; const x = Math.cos(rad) * radius;
const y = Math.sin(rad) * radius; const y = Math.sin(rad) * radius;
const finalX = x - 12; const finalX = x;
const finalY = y - 12; const finalY = y - 12;
return ( return (
@ -383,8 +394,6 @@ function GroupControls({ groupId, zoom }: GroupControlsProps) {
animation: `colorFanIn-${index} 0.25s cubic-bezier(0.34, 1.56, 0.64, 1) forwards`, animation: `colorFanIn-${index} 0.25s cubic-bezier(0.34, 1.56, 0.64, 1) forwards`,
animationDelay: `${index * 0.025}s`, animationDelay: `${index * 0.025}s`,
opacity: 0, opacity: 0,
["--final-x" as string]: `${finalX}px`,
["--final-y" as string]: `${finalY}px`,
}} }}
title={label} title={label}
> >
@ -392,7 +401,7 @@ function GroupControls({ groupId, zoom }: GroupControlsProps) {
@keyframes colorFanIn-${index} { @keyframes colorFanIn-${index} {
0% { 0% {
opacity: 0; opacity: 0;
transform: translate(-12px, 0px) scale(0.3); transform: translate(0px, 0px) scale(0.3);
} }
100% { 100% {
opacity: 1; opacity: 1;
@ -408,11 +417,10 @@ function GroupControls({ groupId, zoom }: GroupControlsProps) {
)} )}
</div> </div>
{/* Lock/Unlock Button */} {/* Lock/Unlock row */}
<button <button
onClick={(e) => { e.stopPropagation(); handleToggleLock(); }} onClick={(e) => { e.stopPropagation(); handleToggleLock(); setShowMenu(false); }}
className="w-6 h-6 rounded-md flex items-center justify-center hover:bg-white/20 text-white/70 hover:text-white transition-colors" className="flex items-center gap-2 px-3 py-1.5 w-full hover:bg-white/10 text-xs text-white/80 transition-colors"
title={group.locked ? "Unlock group" : "Lock group"}
> >
{group.locked ? ( {group.locked ? (
<svg className="w-3.5 h-3.5" 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}>
@ -423,33 +431,22 @@ function GroupControls({ groupId, zoom }: GroupControlsProps) {
<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>
)} )}
<span>{group.locked ? "Unlock" : "Lock"}</span>
</button> </button>
{/* Delete Button */} {/* Delete row */}
<button <button
onClick={(e) => { e.stopPropagation(); handleDelete(); }} onClick={(e) => { e.stopPropagation(); handleDelete(); }}
className="w-6 h-6 rounded-md flex items-center justify-center hover:bg-white/20 text-white/70 hover:text-white transition-colors" className="flex items-center gap-2 px-3 py-1.5 w-full hover:bg-white/10 text-xs text-white/80 transition-colors"
title="Delete group"
> >
<svg className="w-3.5 h-3.5" 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>
<span>Delete</span>
</button> </button>
</div> </div>
)} )}
</div>
{/* Three-dot menu toggle - hidden when menu is open */}
{!showMenu && (
<button
onClick={(e) => { e.stopPropagation(); setShowMenu(true); }}
className="w-6 h-6 rounded-md flex flex-col items-center justify-center gap-[2px] hover:bg-white/20 transition-colors"
title="Group options"
>
<div className="w-[3px] h-[3px] rounded-full bg-white/70" />
<div className="w-[3px] h-[3px] rounded-full bg-white/70" />
<div className="w-[3px] h-[3px] rounded-full bg-white/70" />
</button>
)}
</div> </div>
</div> </div>
</div> </div>

Loading…
Cancel
Save