From bcd2aabf9364ac45df75d22ecf2a7e20b3aad739 Mon Sep 17 00:00:00 2001 From: Maxwell Calkin Date: Sun, 8 Mar 2026 23:31:54 -0400 Subject: [PATCH 1/2] fix: smooth sliding highlight pill for hero service icons 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 #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 --- .../hero/components/icon-button.tsx | 23 ++++++++++++------- .../app/(landing)/components/hero/hero.tsx | 17 ++++++++++---- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/apps/sim/app/(landing)/components/hero/components/icon-button.tsx b/apps/sim/app/(landing)/components/hero/components/icon-button.tsx index c8e523e2f86..8d6d3df8a49 100644 --- a/apps/sim/app/(landing)/components/hero/components/icon-button.tsx +++ b/apps/sim/app/(landing)/components/hero/components/icon-button.tsx @@ -1,23 +1,26 @@ 'use client' import type React from 'react' +import { motion } from 'framer-motion' interface IconButtonProps { children: React.ReactNode onClick?: () => void onMouseEnter?: () => void + onMouseLeave?: () => void style?: React.CSSProperties 'aria-label': string - isAutoHovered?: boolean + isActive?: boolean } export function IconButton({ children, onClick, onMouseEnter, + onMouseLeave, style, 'aria-label': ariaLabel, - isAutoHovered = false, + isActive = false, }: IconButtonProps) { return ( ) } diff --git a/apps/sim/app/(landing)/components/hero/hero.tsx b/apps/sim/app/(landing)/components/hero/hero.tsx index 546dc47627f..3a37688a8d2 100644 --- a/apps/sim/app/(landing)/components/hero/hero.tsx +++ b/apps/sim/app/(landing)/components/hero/hero.tsx @@ -149,7 +149,7 @@ export default function Hero() { */ const [autoHoverIndex, setAutoHoverIndex] = React.useState(1) const [isUserHovering, setIsUserHovering] = React.useState(false) - const [lastHoveredIndex, setLastHoveredIndex] = React.useState(null) + const [hoveredIndex, setHoveredIndex] = React.useState(null) const intervalRef = React.useRef(null) /** @@ -225,6 +225,12 @@ export default function Hero() { } }, [isUserHovering, visibleIconCount]) + /** + * The active icon index used to position the shared highlight pill. + * When the user is hovering, use their hovered icon; otherwise use auto-hover. + */ + const activeIconIndex = isUserHovering && hoveredIndex !== null ? hoveredIndex : autoHoverIndex + /** * Handle mouse enter on icon container */ @@ -240,9 +246,10 @@ export default function Hero() { */ 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) } } @@ -389,9 +396,9 @@ export default function Hero() { key={service.key} aria-label={service.label} onClick={() => handleServiceClick(service.key as keyof typeof SERVICE_TEMPLATES)} - onMouseEnter={() => setLastHoveredIndex(index)} + onMouseEnter={() => setHoveredIndex(index)} style={service.style} - isAutoHovered={!isUserHovering && index === autoHoverIndex} + isActive={index === activeIconIndex} > From 2b3a3f7ff5cac04aa914cfbc2a8c06cbb21dfd2f Mon Sep 17 00:00:00 2001 From: Maxwell Calkin <101308415+MaxwellCalkin@users.noreply.github.com> Date: Mon, 9 Mar 2026 02:07:53 -0400 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20address=20review=20feedback=20?= =?UTF-8?q?=E2=80=94=20stale=20closure=20and=20unused=20prop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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). --- .../app/(landing)/components/hero/components/icon-button.tsx | 3 --- apps/sim/app/(landing)/components/hero/hero.tsx | 5 +++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/sim/app/(landing)/components/hero/components/icon-button.tsx b/apps/sim/app/(landing)/components/hero/components/icon-button.tsx index 8d6d3df8a49..fd6a0901839 100644 --- a/apps/sim/app/(landing)/components/hero/components/icon-button.tsx +++ b/apps/sim/app/(landing)/components/hero/components/icon-button.tsx @@ -7,7 +7,6 @@ interface IconButtonProps { children: React.ReactNode onClick?: () => void onMouseEnter?: () => void - onMouseLeave?: () => void style?: React.CSSProperties 'aria-label': string isActive?: boolean @@ -17,7 +16,6 @@ export function IconButton({ children, onClick, onMouseEnter, - onMouseLeave, style, 'aria-label': ariaLabel, isActive = false, @@ -28,7 +26,6 @@ export function IconButton({ aria-label={ariaLabel} onClick={onClick} onMouseEnter={onMouseEnter} - onMouseLeave={onMouseLeave} className='relative flex items-center justify-center rounded-xl p-2 outline-none' style={style} > diff --git a/apps/sim/app/(landing)/components/hero/hero.tsx b/apps/sim/app/(landing)/components/hero/hero.tsx index 3a37688a8d2..7817c082886 100644 --- a/apps/sim/app/(landing)/components/hero/hero.tsx +++ b/apps/sim/app/(landing)/components/hero/hero.tsx @@ -245,11 +245,12 @@ export default function Hero() { * Handle mouse leave on icon container */ const handleIconContainerMouseLeave = () => { + const lastIndex = hoveredIndex setIsUserHovering(false) setHoveredIndex(null) // Start from the next icon after the last hovered one - if (hoveredIndex !== null) { - setAutoHoverIndex((hoveredIndex + 1) % visibleIconCount) + if (lastIndex !== null) { + setAutoHoverIndex((lastIndex + 1) % visibleIconCount) } }