|
| 1 | +import type { ReactNode } from 'react' |
| 2 | +import { ChipTag, cn } from '@sim/emcn' |
| 3 | +import { ArrowRight } from 'lucide-react' |
| 4 | +import Image from 'next/image' |
| 5 | +import Link from 'next/link' |
| 6 | + |
| 7 | +interface FeatureCardProps { |
| 8 | + /** Capability name shown as a chip tag pinned to the card's top-right corner. */ |
| 9 | + eyebrow: string |
| 10 | + /** The beat's headline (`<h3>` - the section owns the single `<h2>`). */ |
| 11 | + title: string |
| 12 | + /** Supporting copy beneath the headline. */ |
| 13 | + description: string |
| 14 | + /** Optional trailing link (e.g. the feature's platform page). */ |
| 15 | + href?: string |
| 16 | + /** Label for {@link href}. */ |
| 17 | + linkLabel?: string |
| 18 | + /** Backdrop image under the floating callout (public path). */ |
| 19 | + backdropSrc: string |
| 20 | + /** |
| 21 | + * Which side the media stage sits on from `lg` up (cards alternate down the |
| 22 | + * section, Cursor-style). Below `lg` the card always stacks media-first. |
| 23 | + */ |
| 24 | + mediaSide?: 'left' | 'right' |
| 25 | + /** |
| 26 | + * Square the card's bottom corners so its bottom edge merges with a |
| 27 | + * full-bleed divider drawn at the same line (the section's last card). |
| 28 | + */ |
| 29 | + flushBottom?: boolean |
| 30 | + /** The elevated real-UI callout floating over the backdrop. */ |
| 31 | + children: ReactNode |
| 32 | +} |
| 33 | + |
| 34 | +/** |
| 35 | + * Cursor-style feature card - one large OUTLINED container (a light |
| 36 | + * `--border` hairline on a transparent ground, no grey fill) holding a media |
| 37 | + * stage (a painted backdrop with the beat's real-UI callout floating over it) |
| 38 | + * and a vertically-centered copy column: `<h3>`, muted description, and an |
| 39 | + * optional arrow link. `mediaSide` picks which side the media sits on so the |
| 40 | + * cards can alternate down the section. The beat's name sits as a borderless |
| 41 | + * grey-filled {@link ChipTag} pinned to the card's top corner on the COPY |
| 42 | + * side (top-right when media is left, top-left when media is right), just |
| 43 | + * inside the outline - never floating over the media image. |
| 44 | + * |
| 45 | + * Below `lg` the card stacks - media on top, copy beneath - and the media |
| 46 | + * shortens so the card stays scannable in the compact grid. |
| 47 | + */ |
| 48 | +export function FeatureCard({ |
| 49 | + eyebrow, |
| 50 | + title, |
| 51 | + description, |
| 52 | + href, |
| 53 | + linkLabel, |
| 54 | + backdropSrc, |
| 55 | + mediaSide = 'left', |
| 56 | + flushBottom = false, |
| 57 | + children, |
| 58 | +}: FeatureCardProps) { |
| 59 | + const mediaRight = mediaSide === 'right' |
| 60 | + return ( |
| 61 | + <article |
| 62 | + className={cn( |
| 63 | + 'relative grid gap-10 rounded-[10px] border border-[var(--border)] p-4 max-lg:grid-cols-1 max-lg:gap-6', |
| 64 | + mediaRight ? 'grid-cols-[386px_1fr]' : 'grid-cols-[1fr_386px]', |
| 65 | + flushBottom && 'rounded-b-none' |
| 66 | + )} |
| 67 | + > |
| 68 | + <ChipTag |
| 69 | + variant='mono' |
| 70 | + className={cn('absolute top-4 z-10', mediaRight ? 'left-4' : 'right-4')} |
| 71 | + > |
| 72 | + {eyebrow} |
| 73 | + </ChipTag> |
| 74 | + <div |
| 75 | + aria-hidden='true' |
| 76 | + className={cn( |
| 77 | + 'relative h-[650px] overflow-hidden rounded-[4px] max-sm:h-[280px] max-lg:order-1 max-lg:h-[360px]', |
| 78 | + mediaRight && 'lg:order-2' |
| 79 | + )} |
| 80 | + > |
| 81 | + <Image |
| 82 | + src={backdropSrc} |
| 83 | + alt='' |
| 84 | + fill |
| 85 | + sizes='(max-width: 1460px) 70vw, 900px' |
| 86 | + className='object-cover' |
| 87 | + /> |
| 88 | + <div className='absolute inset-0 flex items-center justify-center p-4 [&>*]:max-w-full'> |
| 89 | + {children} |
| 90 | + </div> |
| 91 | + </div> |
| 92 | + |
| 93 | + <div |
| 94 | + className={cn( |
| 95 | + 'flex flex-col justify-center max-lg:order-2 max-lg:pb-2', |
| 96 | + mediaRight ? 'pl-4 max-lg:pl-0' : 'pr-4 max-lg:pr-0' |
| 97 | + )} |
| 98 | + > |
| 99 | + <h3 className='text-balance font-medium text-[22px] text-[var(--text-primary)] leading-[1.3] max-sm:text-[20px]'> |
| 100 | + {title} |
| 101 | + </h3> |
| 102 | + <p className='mt-3 text-pretty text-[15px] text-[var(--text-muted)] leading-[1.6]'> |
| 103 | + {description} |
| 104 | + </p> |
| 105 | + {href && linkLabel && ( |
| 106 | + <Link |
| 107 | + href={href} |
| 108 | + className='mt-5 flex items-center gap-1.5 text-[15px] text-[var(--text-body)] transition-colors hover-hover:text-[var(--text-primary)]' |
| 109 | + > |
| 110 | + {linkLabel} |
| 111 | + <ArrowRight className='size-[15px]' /> |
| 112 | + </Link> |
| 113 | + )} |
| 114 | + </div> |
| 115 | + </article> |
| 116 | + ) |
| 117 | +} |
0 commit comments