fix: smooth sliding highlight pill for hero service icons#3481
fix: smooth sliding highlight pill for hero service icons#3481MaxwellCalkin wants to merge 2 commits intosimstudioai:mainfrom
Conversation
Replace per-icon hover state (abrupt border/shadow jump) with a single shared highlight pill that slides smoothly between icons using framer-motion layoutId animation. The pill follows the cursor on hover and continues the auto-cycle on mouse leave. Fixes simstudioai#3468 > This PR was authored by an AI (Claude Opus 4.6, Anthropic). See > https://www.maxcalkin.com/ai for transparency details. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PR SummaryLow Risk Overview
Written by Cursor Bugbot for commit 2b3a3f7. This will update automatically on new commits. Configure here. |
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Greptile SummaryThis PR replaces the per-icon CSS hover state (border/shadow toggled individually) on the hero service icons with a single framer-motion shared-layout pill ( Key changes:
Issues found:
Confidence Score: 3/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant ContainerDiv
participant IconButton
participant HeroState
Note over HeroState: autoHoverIndex advances every 2 s (isUserHovering = false)
User->>ContainerDiv: mouseenter
ContainerDiv->>HeroState: setIsUserHovering(true) + clearInterval()
User->>IconButton: mouseenter (index N)
IconButton->>HeroState: setHoveredIndex(N)
Note over HeroState: activeIconIndex = N (user hover wins)
User->>IconButton: mouseenter (index M)
IconButton->>HeroState: setHoveredIndex(M)
Note over HeroState: activeIconIndex = M
User->>ContainerDiv: mouseleave
ContainerDiv->>HeroState: setIsUserHovering(false), setHoveredIndex(null)
ContainerDiv->>HeroState: setAutoHoverIndex((M+1) % visibleIconCount) [may be stale]
Note over HeroState: Auto-cycle resumes from icon M+1
Last reviewed commit: bcd2aab |
| const handleIconContainerMouseLeave = () => { | ||
| setIsUserHovering(false) | ||
| setHoveredIndex(null) | ||
| // Start from the next icon after the last hovered one | ||
| if (lastHoveredIndex !== null) { | ||
| setAutoHoverIndex((lastHoveredIndex + 1) % visibleIconCount) | ||
| if (hoveredIndex !== null) { | ||
| setAutoHoverIndex((hoveredIndex + 1) % visibleIconCount) | ||
| } |
There was a problem hiding this comment.
Stale closure: hoveredIndex may be read before its setHoveredIndex update is applied
In React 18 with automatic batching, setHoveredIndex(index) (called in the icon's onMouseEnter) and the container's onMouseLeave handler can be triggered by two separate, rapid browser events. If the user hovers an icon and immediately leaves the container before React has re-rendered, handleIconContainerMouseLeave will execute with the closure's stale hoveredIndex value (the one from the last render, which is still null or a previous index). The auto-cycle will then resume from the wrong icon.
The reliable fix is to mirror the state into a useRef so the handler always reads the latest value:
const hoveredIndexRef = React.useRef<number | null>(null)
// in the onMouseEnter passed to each IconButton:
onMouseEnter={() => {
setHoveredIndex(index)
hoveredIndexRef.current = index
}}
// in handleIconContainerMouseLeave:
const handleIconContainerMouseLeave = () => {
setIsUserHovering(false)
setHoveredIndex(null)
if (hoveredIndexRef.current !== null) {
setAutoHoverIndex((hoveredIndexRef.current + 1) % visibleIconCount)
}
hoveredIndexRef.current = null
}
| children: React.ReactNode | ||
| onClick?: () => void | ||
| onMouseEnter?: () => void | ||
| onMouseLeave?: () => void |
There was a problem hiding this comment.
Unused onMouseLeave prop
onMouseLeave is declared in IconButtonProps and forwarded to the <button> element, but hero.tsx never passes this prop when rendering <IconButton>. The mouse-leave behaviour is handled entirely by the container div's onMouseLeave. This dead prop adds noise to the interface and may confuse future maintainers.
| onMouseLeave?: () => void | |
| onMouseLeave?: () => void |
Consider removing it unless there's a planned use-case, or add a brief comment explaining why it's reserved for future use.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
- Save hoveredIndex to local variable before clearing to avoid confusing stale closure read (Cursor Bugbot + Greptile feedback) - Remove unused onMouseLeave prop from IconButtonProps since mouse-leave is handled by the container div AI Disclosure: This commit was authored by Claude Opus 4.6 (Anthropic), an AI agent operated by Maxwell Calkin (@MaxwellCalkin).
Summary
Fixes #3468
The service integration icons on the landing page hero use per-icon hover states, causing the border/shadow highlight to jump abruptly between icons. This replaces that with a single shared highlight pill that slides smoothly between icons using framer-motion's
layoutIdanimation.Changes
icon-button.tsx: Replace CSS border/shadow toggle with amotion.divpill usinglayoutId='icon-highlight-pill'for shared layout animation. Spring physics (stiffness: 400, damping: 30) produce a snappy but smooth slide.hero.tsx: ConsolidatelastHoveredIndexintohoveredIndexand compute a singleactiveIconIndex(manual hover takes priority over auto-cycle). On mouse leave, the auto-cycle resumes from the next icon after the last hovered one.Behavior