|
1 | 1 | 'use client' |
2 | 2 |
|
3 | 3 | import React from 'react' |
| 4 | +import { motion } from 'framer-motion' |
4 | 5 | import { ArrowUp, CodeIcon } from 'lucide-react' |
5 | 6 | import { useRouter } from 'next/navigation' |
6 | 7 | import { type Edge, type Node, Position } from 'reactflow' |
@@ -152,6 +153,16 @@ export default function Hero() { |
152 | 153 | const [lastHoveredIndex, setLastHoveredIndex] = React.useState<number | null>(null) |
153 | 154 | const intervalRef = React.useRef<NodeJS.Timeout | null>(null) |
154 | 155 |
|
| 156 | + const iconRowRef = React.useRef<HTMLDivElement | null>(null) |
| 157 | + const buttonRefs = React.useRef<(HTMLDivElement | null)[]>([]) |
| 158 | + const [pillLayout, setPillLayout] = React.useState<{ |
| 159 | + left: number |
| 160 | + top: number |
| 161 | + width: number |
| 162 | + height: number |
| 163 | + } | null>(null) |
| 164 | + const [layoutVersion, setLayoutVersion] = React.useState(0) |
| 165 | + |
155 | 166 | /** |
156 | 167 | * Handle service icon click to populate textarea with template |
157 | 168 | */ |
@@ -246,6 +257,29 @@ export default function Hero() { |
246 | 257 | } |
247 | 258 | } |
248 | 259 |
|
| 260 | + const activeIconIndex = |
| 261 | + isUserHovering && lastHoveredIndex !== null ? lastHoveredIndex : autoHoverIndex |
| 262 | + |
| 263 | + React.useLayoutEffect(() => { |
| 264 | + const container = iconRowRef.current |
| 265 | + const target = buttonRefs.current[activeIconIndex] |
| 266 | + if (!container || !target) return |
| 267 | + const cr = container.getBoundingClientRect() |
| 268 | + const tr = target.getBoundingClientRect() |
| 269 | + setPillLayout({ |
| 270 | + left: tr.left - cr.left, |
| 271 | + top: tr.top - cr.top, |
| 272 | + width: tr.width, |
| 273 | + height: tr.height, |
| 274 | + }) |
| 275 | + }, [activeIconIndex, layoutVersion, visibleIconCount]) |
| 276 | + |
| 277 | + React.useEffect(() => { |
| 278 | + const onResize = () => setLayoutVersion((v) => v + 1) |
| 279 | + window.addEventListener('resize', onResize) |
| 280 | + return () => window.removeEventListener('resize', onResize) |
| 281 | + }, []) |
| 282 | + |
249 | 283 | /** |
250 | 284 | * Handle form submission |
251 | 285 | */ |
@@ -377,24 +411,48 @@ export default function Hero() { |
377 | 411 | Build and deploy AI agent workflows |
378 | 412 | </p> |
379 | 413 | <div |
380 | | - className='flex items-center justify-center gap-[2px] pt-[18px] sm:pt-[32px]' |
| 414 | + ref={iconRowRef} |
| 415 | + className='relative flex items-center justify-center gap-[2px] pt-[18px] sm:pt-[32px]' |
381 | 416 | onMouseEnter={handleIconContainerMouseEnter} |
382 | 417 | onMouseLeave={handleIconContainerMouseLeave} |
383 | 418 | > |
384 | | - {/* Service integration buttons */} |
| 419 | + {pillLayout !== null && ( |
| 420 | + <motion.div |
| 421 | + className='pointer-events-none absolute rounded-xl border border-[#E5E5E5] shadow-[0_2px_4px_0_rgba(0,0,0,0.08)]' |
| 422 | + initial={false} |
| 423 | + animate={{ |
| 424 | + left: pillLayout.left, |
| 425 | + top: pillLayout.top, |
| 426 | + width: pillLayout.width, |
| 427 | + height: pillLayout.height, |
| 428 | + }} |
| 429 | + transition={{ |
| 430 | + type: 'spring', |
| 431 | + stiffness: 325, |
| 432 | + damping: 33, |
| 433 | + mass: 1.12, |
| 434 | + }} |
| 435 | + /> |
| 436 | + )} |
385 | 437 | {serviceIcons.slice(0, visibleIconCount).map((service, index) => { |
386 | 438 | const Icon = service.icon |
387 | 439 | return ( |
388 | | - <IconButton |
| 440 | + <div |
389 | 441 | key={service.key} |
390 | | - aria-label={service.label} |
391 | | - onClick={() => handleServiceClick(service.key as keyof typeof SERVICE_TEMPLATES)} |
392 | | - onMouseEnter={() => setLastHoveredIndex(index)} |
393 | | - style={service.style} |
394 | | - isAutoHovered={!isUserHovering && index === autoHoverIndex} |
| 442 | + ref={(el) => { |
| 443 | + buttonRefs.current[index] = el |
| 444 | + }} |
395 | 445 | > |
396 | | - <Icon className='h-5 w-5 sm:h-6 sm:w-6' /> |
397 | | - </IconButton> |
| 446 | + <IconButton |
| 447 | + aria-label={service.label} |
| 448 | + onClick={() => handleServiceClick(service.key as keyof typeof SERVICE_TEMPLATES)} |
| 449 | + onMouseEnter={() => setLastHoveredIndex(index)} |
| 450 | + style={service.style} |
| 451 | + isAutoHovered={!isUserHovering && index === autoHoverIndex} |
| 452 | + > |
| 453 | + <Icon className='h-5 w-5 sm:h-6 sm:w-6' /> |
| 454 | + </IconButton> |
| 455 | + </div> |
398 | 456 | ) |
399 | 457 | })} |
400 | 458 | </div> |
|
0 commit comments