Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b7e377e
v0.5.91: docs i18n, turborepo upgrade
waleedlatif1 Feb 16, 2026
da46a38
v0.5.92: shortlinks, copilot scrolling stickiness, pagination
waleedlatif1 Feb 17, 2026
fdca736
v0.5.93: NextJS config changes, MCP and Blocks whitelisting, copilot …
waleedlatif1 Feb 18, 2026
15ace5e
v0.5.94: vercel integration, folder insertion, migrated tracking redi…
waleedlatif1 Feb 19, 2026
67aa4bb
v0.5.95: gemini 3.1 pro, cloudflare, dataverse, revenuecat, redis, up…
waleedlatif1 Feb 20, 2026
34d92fa
v0.5.96: sim oauth provider, slack ephemeral message tool and blockki…
waleedlatif1 Feb 21, 2026
115f04e
v0.5.97: oidc discovery for copilot mcp
waleedlatif1 Feb 21, 2026
0d86ea0
v0.5.98: change detection improvements, rate limit and code execution…
waleedlatif1 Feb 22, 2026
af59234
v0.5.99: local dev improvements, live workflow logs in terminal
waleedlatif1 Feb 23, 2026
67f8a68
v0.5.100: multiple credentials, 40% speedup, gong, attio, audit log i…
waleedlatif1 Feb 25, 2026
4fd0989
v0.5.101: circular dependency mitigation, confluence enhancements, go…
waleedlatif1 Feb 26, 2026
0d2e6ff
v0.5.102: new integrations, new tools, ci speedups, memory leak instr…
waleedlatif1 Feb 28, 2026
e07e3c3
v0.5.103: memory util instrumentation, API docs, amplitude, google pa…
waleedlatif1 Mar 2, 2026
f1ec5fe
v0.5.104: memory improvements, nested subflows, careers page redirect…
waleedlatif1 Mar 4, 2026
70c36cb
v0.5.105: slack remove reaction, nested subflow locks fix, servicenow…
waleedlatif1 Mar 5, 2026
3ce9475
v0.5.106: condition block and legacy kbs fixes, GPT 5.4
icecrasher321 Mar 6, 2026
6586c5c
v0.5.107: new reddit, slack tools
waleedlatif1 Mar 6, 2026
8c0a2e0
v0.5.108: workflow input params in agent tools, bun upgrade, dropdown…
icecrasher321 Mar 7, 2026
782e9f6
feat: smoother animation for icons
adithyaakrishna Mar 8, 2026
48782d2
chore: fix state lag on hover
adithyaakrishna Mar 8, 2026
b4b7a86
chore: made requested changes
adithyaakrishna Mar 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interface IconButtonProps {
onMouseEnter?: () => void
style?: React.CSSProperties
'aria-label': string
isAutoHovered?: boolean
highlightFromParent?: boolean
}

export function IconButton({
Expand All @@ -17,19 +17,19 @@ export function IconButton({
onMouseEnter,
style,
'aria-label': ariaLabel,
isAutoHovered = false,
highlightFromParent = false,
}: IconButtonProps) {
const hoverHighlight = highlightFromParent
? ''
: 'hover:border-[#E5E5E5] hover:shadow-[0_2px_4px_0_rgba(0,0,0,0.08)]'

return (
<button
type='button'
aria-label={ariaLabel}
onClick={onClick}
onMouseEnter={onMouseEnter}
className={`flex items-center justify-center rounded-xl border p-2 outline-none transition-all duration-300 ${
isAutoHovered
? 'border-[#E5E5E5] shadow-[0_2px_4px_0_rgba(0,0,0,0.08)]'
: 'border-transparent hover:border-[#E5E5E5] hover:shadow-[0_2px_4px_0_rgba(0,0,0,0.08)]'
}`}
className={`flex items-center justify-center rounded-xl border border-transparent p-2 outline-none transition-all duration-300 ${hoverHighlight}`}
style={style}
>
{children}
Expand Down
120 changes: 102 additions & 18 deletions apps/sim/app/(landing)/components/hero/hero.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client'

import React from 'react'
import { motion } from 'framer-motion'
import { ArrowUp, CodeIcon } from 'lucide-react'
import { useRouter } from 'next/navigation'
import { type Edge, type Node, Position } from 'reactflow'
Expand Down Expand Up @@ -150,12 +151,28 @@ export default function Hero() {
const [autoHoverIndex, setAutoHoverIndex] = React.useState(1)
const [isUserHovering, setIsUserHovering] = React.useState(false)
const [lastHoveredIndex, setLastHoveredIndex] = React.useState<number | null>(null)
const [selectedIconIndex, setSelectedIconIndex] = React.useState<number | null>(null)
const intervalRef = React.useRef<NodeJS.Timeout | null>(null)

const iconRowRef = React.useRef<HTMLDivElement | null>(null)
const buttonRefs = React.useRef<(HTMLDivElement | null)[]>([])
const [pillLayout, setPillLayout] = React.useState<{
left: number
top: number
width: number
height: number
} | null>(null)
const [layoutVersion, setLayoutVersion] = React.useState(0)

/**
* Handle service icon click to populate textarea with template
*/
const handleServiceClick = (service: keyof typeof SERVICE_TEMPLATES) => {
const handleServiceClick = (index: number, service: keyof typeof SERVICE_TEMPLATES) => {
setSelectedIconIndex(index)
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
setTextValue(SERVICE_TEMPLATES[service])
}

Expand All @@ -177,6 +194,21 @@ export default function Hero() {
return () => window.removeEventListener('resize', updateVisibleIcons)
}, [])

React.useEffect(() => {
const maxIndex = visibleIconCount - 1
setAutoHoverIndex((i) => Math.min(i, maxIndex))
setLastHoveredIndex((idx) =>
idx !== null ? Math.min(idx, maxIndex) : null
)
setSelectedIconIndex((idx) =>
idx !== null ? Math.min(idx, maxIndex) : null
)
}, [visibleIconCount])
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clamped selectedIconIndex mismatches textarea template content

Low Severity

When visibleIconCount shrinks (e.g., resizing from desktop to mobile), selectedIconIndex is clamped to maxIndex but textValue is left unchanged. This causes the sliding pill to highlight a different service icon than the one whose template is displayed in the textarea. For instance, clicking icon 10 (Google Sheets) then resizing to mobile clamps selectedIconIndex to 5 (Supabase), so the pill highlights Supabase while the textarea still shows the Google Sheets template.

Additional Locations (1)

Fix in Cursor Fix in Web


React.useEffect(() => {
if (textValue.trim().length === 0) setSelectedIconIndex(null)
}, [textValue])

/**
* Service icons array for easier indexing
*/
Expand Down Expand Up @@ -205,25 +237,20 @@ export default function Hero() {
* Auto-hover animation effect
*/
React.useEffect(() => {
// Start the interval when component mounts
const startInterval = () => {
intervalRef.current = setInterval(() => {
setAutoHoverIndex((prev) => (prev + 1) % visibleIconCount)
}, 2000)
}

// Only run interval when user is not hovering
if (!isUserHovering) {
startInterval()
}
if (selectedIconIndex === null && !isUserHovering) startInterval()

// Cleanup on unmount or when hovering state changes
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
}
}
}, [isUserHovering, visibleIconCount])
}, [selectedIconIndex, isUserHovering, visibleIconCount])

/**
* Handle mouse enter on icon container
Expand All @@ -246,6 +273,37 @@ export default function Hero() {
}
}

const activeIconIndex =
selectedIconIndex !== null
? selectedIconIndex
: isUserHovering && lastHoveredIndex !== null
? lastHoveredIndex
: autoHoverIndex
const pillTargetIndex = Math.max(
0,
Math.min(activeIconIndex, visibleIconCount - 1)
)

React.useLayoutEffect(() => {
const container = iconRowRef.current
const target = buttonRefs.current[pillTargetIndex]
if (!container || !target) return
const cr = container.getBoundingClientRect()
const tr = target.getBoundingClientRect()
setPillLayout({
left: tr.left - cr.left,
top: tr.top - cr.top,
width: tr.width,
height: tr.height,
})
}, [pillTargetIndex, layoutVersion, visibleIconCount])

React.useEffect(() => {
const onResize = () => setLayoutVersion((v) => v + 1)
window.addEventListener('resize', onResize)
return () => window.removeEventListener('resize', onResize)
}, [])

/**
* Handle form submission
*/
Expand Down Expand Up @@ -377,24 +435,50 @@ export default function Hero() {
Build and deploy AI agent workflows
</p>
<div
className='flex items-center justify-center gap-[2px] pt-[18px] sm:pt-[32px]'
ref={iconRowRef}
className='relative flex items-center justify-center gap-[2px] pt-[18px] sm:pt-[32px]'
onMouseEnter={handleIconContainerMouseEnter}
onMouseLeave={handleIconContainerMouseLeave}
>
{/* Service integration buttons */}
{pillLayout !== null && (
<motion.div
className='pointer-events-none absolute rounded-xl border border-[#E5E5E5] shadow-[0_2px_4px_0_rgba(0,0,0,0.08)]'
initial={false}
animate={{
left: pillLayout.left,
top: pillLayout.top,
width: pillLayout.width,
height: pillLayout.height,
}}
transition={{
type: 'spring',
stiffness: 325,
damping: 33,
mass: 1.12,
}}
/>
)}
{serviceIcons.slice(0, visibleIconCount).map((service, index) => {
const Icon = service.icon
return (
<IconButton
<div
key={service.key}
aria-label={service.label}
onClick={() => handleServiceClick(service.key as keyof typeof SERVICE_TEMPLATES)}
onMouseEnter={() => setLastHoveredIndex(index)}
style={service.style}
isAutoHovered={!isUserHovering && index === autoHoverIndex}
ref={(el) => {
buttonRefs.current[index] = el
}}
>
<Icon className='h-5 w-5 sm:h-6 sm:w-6' />
</IconButton>
<IconButton
aria-label={service.label}
onClick={() =>
handleServiceClick(index, service.key as keyof typeof SERVICE_TEMPLATES)
}
onMouseEnter={() => setLastHoveredIndex(index)}
style={service.style}
highlightFromParent
>
<Icon className='h-5 w-5 sm:h-6 sm:w-6' />
</IconButton>
Comment on lines +470 to +480
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IconButton applies border-[#E5E5E5] shadow-[0_2px_4px_0_rgba(0,0,0,0.08)] immediately when isAutoHovered=true (with a 300ms CSS transition). The sliding pill carries the same border/shadow. During the spring animation from icon A to icon B:

  1. Icon B's button immediately renders its own border (because isAutoHovered flipped to true)
  2. The pill is still mid-slide between A and B

This creates two visible bordered highlights simultaneously — one static at B (the button's own border) and one sliding (the pill) — which defeats the purpose of the smooth transition.

Since the pill is now the sole source of the highlight, consider suppressing the isAutoHovered border/shadow on IconButton:

Suggested change
<IconButton
aria-label={service.label}
onClick={() => handleServiceClick(service.key as keyof typeof SERVICE_TEMPLATES)}
onMouseEnter={() => setLastHoveredIndex(index)}
style={service.style}
isAutoHovered={!isUserHovering && index === autoHoverIndex}
>
<Icon className='h-5 w-5 sm:h-6 sm:w-6' />
</IconButton>
<IconButton
aria-label={service.label}
onClick={() => handleServiceClick(service.key as keyof typeof SERVICE_TEMPLATES)}
onMouseEnter={() => setLastHoveredIndex(index)}
style={service.style}
isAutoHovered={false}
>
<Icon className='h-5 w-5 sm:h-6 sm:w-6' />
</IconButton>

Alternatively, update IconButton to not apply the border/shadow when isAutoHovered=true so the pill is the only visual indicator.

</div>
)
})}
</div>
Expand Down