Skip to content

Commit fbdae9e

Browse files
committed
improvement: features, footer, tab modals
1 parent 8df13e1 commit fbdae9e

File tree

8 files changed

+430
-125
lines changed

8 files changed

+430
-125
lines changed

apps/sim/app/(home)/components/collaboration/collaboration.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ function DotGrid({ className, cols, rows, gap = 0 }: DotGridProps) {
2525
}}
2626
>
2727
{Array.from({ length: cols * rows }, (_, i) => (
28-
<div key={i} className='h-[2px] w-[2px] rounded-full bg-[#2A2A2A]' />
28+
<div key={i} className='h-[1.5px] w-[1.5px] rounded-full bg-[#2A2A2A]' />
2929
))}
3030
</div>
3131
)
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
'use client'
2+
3+
import { type SVGProps, useRef } from 'react'
4+
import { motion, useInView } from 'framer-motion'
5+
import { ChevronDown } from '@/components/emcn'
6+
import { Database, File, Library, Table } from '@/components/emcn/icons'
7+
import {
8+
AnthropicIcon,
9+
GeminiIcon,
10+
GmailIcon,
11+
GroqIcon,
12+
HubspotIcon,
13+
OpenAIIcon,
14+
SalesforceIcon,
15+
SlackIcon,
16+
xAIIcon,
17+
} from '@/components/icons'
18+
19+
interface IconEntry {
20+
key: string
21+
icon: React.ComponentType<SVGProps<SVGSVGElement>>
22+
label: string
23+
top: string
24+
left: string
25+
color?: string
26+
}
27+
28+
const SCATTERED_ICONS: IconEntry[] = [
29+
{ key: 'slack', icon: SlackIcon, label: 'Slack', top: '8%', left: '14%' },
30+
{ key: 'openai', icon: OpenAIIcon, label: 'OpenAI', top: '8%', left: '44%' },
31+
{ key: 'anthropic', icon: AnthropicIcon, label: 'Anthropic', top: '10%', left: '78%' },
32+
{ key: 'gmail', icon: GmailIcon, label: 'Gmail', top: '24%', left: '90%' },
33+
{ key: 'salesforce', icon: SalesforceIcon, label: 'Salesforce', top: '28%', left: '6%' },
34+
{ key: 'table', icon: Table, label: 'Tables', top: '22%', left: '30%' },
35+
{ key: 'xai', icon: xAIIcon, label: 'xAI', top: '26%', left: '66%' },
36+
{ key: 'hubspot', icon: HubspotIcon, label: 'HubSpot', top: '55%', left: '4%', color: '#FF7A59' },
37+
{ key: 'database', icon: Database, label: 'Database', top: '74%', left: '68%' },
38+
{ key: 'file', icon: File, label: 'Files', top: '70%', left: '18%' },
39+
{ key: 'gemini', icon: GeminiIcon, label: 'Gemini', top: '58%', left: '86%' },
40+
{ key: 'logs', icon: Library, label: 'Logs', top: '86%', left: '44%' },
41+
{ key: 'groq', icon: GroqIcon, label: 'Groq', top: '90%', left: '82%' },
42+
]
43+
44+
const EXPLODE_STAGGER = 0.04
45+
const EXPLODE_BASE_DELAY = 0.1
46+
47+
export function FeaturesPreview() {
48+
const containerRef = useRef<HTMLDivElement>(null)
49+
const inView = useInView(containerRef, { once: true, margin: '-80px' })
50+
51+
return (
52+
<div ref={containerRef} className='relative h-[560px] w-full overflow-hidden'>
53+
<div
54+
aria-hidden='true'
55+
className='absolute inset-0'
56+
style={{
57+
backgroundImage: 'radial-gradient(circle, #D4D4D4 0.75px, transparent 0.75px)',
58+
backgroundSize: '12px 12px',
59+
maskImage: 'radial-gradient(ellipse 70% 65% at 48% 50%, black 30%, transparent 80%)',
60+
WebkitMaskImage:
61+
'radial-gradient(ellipse 70% 65% at 48% 50%, black 30%, transparent 80%)',
62+
}}
63+
/>
64+
65+
{SCATTERED_ICONS.map(({ key, icon: Icon, label, top, left, color }, index) => {
66+
const explodeDelay = EXPLODE_BASE_DELAY + index * EXPLODE_STAGGER
67+
68+
return (
69+
<motion.div
70+
key={key}
71+
className='absolute flex items-center justify-center rounded-xl border border-[#E5E5E5] bg-white p-[10px] shadow-[0_2px_4px_0_rgba(0,0,0,0.06)]'
72+
initial={{ top: '50%', left: '50%', opacity: 0, scale: 0, x: '-50%', y: '-50%' }}
73+
animate={inView ? { top, left, opacity: 1, scale: 1, x: '-50%', y: '-50%' } : undefined}
74+
transition={{
75+
type: 'spring',
76+
stiffness: 50,
77+
damping: 12,
78+
delay: explodeDelay,
79+
}}
80+
style={{ color }}
81+
aria-label={label}
82+
>
83+
<Icon className='h-6 w-6' />
84+
</motion.div>
85+
)
86+
})}
87+
88+
<motion.div
89+
className='absolute top-1/2 left-[48%]'
90+
initial={{ opacity: 0, x: '-50%', y: '-50%' }}
91+
animate={inView ? { opacity: 1, x: '-50%', y: '-50%' } : undefined}
92+
transition={{ duration: 0.4, ease: 'easeOut', delay: 0 }}
93+
>
94+
<div className='flex h-[36px] items-center gap-[8px] rounded-[8px] border border-[#E5E5E5] bg-white px-[10px] shadow-[0_2px_6px_0_rgba(0,0,0,0.08)]'>
95+
<div className='flex h-[22px] w-[22px] flex-shrink-0 items-center justify-center rounded-[5px] bg-[#1e1e1e]'>
96+
<svg width='11' height='11' viewBox='0 0 10 10' fill='none'>
97+
<path
98+
d='M1 9C1 4.58 4.58 1 9 1'
99+
stroke='white'
100+
strokeWidth='1.8'
101+
strokeLinecap='round'
102+
/>
103+
</svg>
104+
</div>
105+
<span className='whitespace-nowrap font-medium font-season text-[#1C1C1C] text-[13px] tracking-[0.02em]'>
106+
My Workspace
107+
</span>
108+
<ChevronDown className='h-[8px] w-[10px] flex-shrink-0 text-[#999]' />
109+
</div>
110+
</motion.div>
111+
</div>
112+
)
113+
}

apps/sim/app/(home)/components/features/features.tsx

Lines changed: 116 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
'use client'
22

3-
import { useState } from 'react'
3+
import { useRef, useState } from 'react'
4+
import { type MotionValue, motion, useScroll, useTransform } from 'framer-motion'
45
import Image from 'next/image'
56
import Link from 'next/link'
67
import { Badge, ChevronDown } from '@/components/emcn'
8+
import { FeaturesPreview } from '@/app/(home)/components/features/components/features-preview'
79

810
function hexToRgba(hex: string, alpha: number): string {
911
const r = Number.parseInt(hex.slice(1, 3), 16)
@@ -115,6 +117,25 @@ const FEATURE_TABS = [
115117
},
116118
]
117119

120+
const HEADING_TEXT = 'Everything you need to build, deploy, and manage AI agents. '
121+
const HEADING_LETTERS = HEADING_TEXT.split('')
122+
123+
const LETTER_REVEAL_SPAN = 0.85
124+
const LETTER_FADE_IN = 0.04
125+
126+
interface ScrollLetterProps {
127+
scrollYProgress: MotionValue<number>
128+
charIndex: number
129+
children: string
130+
}
131+
132+
function ScrollLetter({ scrollYProgress, charIndex, children }: ScrollLetterProps) {
133+
const threshold = (charIndex / HEADING_LETTERS.length) * LETTER_REVEAL_SPAN
134+
const opacity = useTransform(scrollYProgress, [threshold, threshold + LETTER_FADE_IN], [0.4, 1])
135+
136+
return <motion.span style={{ opacity }}>{children}</motion.span>
137+
}
138+
118139
function DotGrid({
119140
cols,
120141
rows,
@@ -139,20 +160,26 @@ function DotGrid({
139160
}}
140161
>
141162
{Array.from({ length: cols * rows }, (_, i) => (
142-
<div key={i} className='h-[2px] w-[2px] rounded-full bg-[#DEDEDE]' />
163+
<div key={i} className='h-[1.5px] w-[1.5px] rounded-full bg-[#DEDEDE]' />
143164
))}
144165
</div>
145166
)
146167
}
147168

148169
export default function Features() {
170+
const sectionRef = useRef<HTMLDivElement>(null)
149171
const [activeTab, setActiveTab] = useState(0)
150172

173+
const { scrollYProgress } = useScroll({
174+
target: sectionRef,
175+
offset: ['start 0.9', 'start 0.2'],
176+
})
177+
151178
return (
152179
<section
153180
id='features'
154181
aria-labelledby='features-heading'
155-
className='relative overflow-hidden bg-[#F6F6F6] pb-[144px]'
182+
className='relative overflow-hidden bg-[#F6F6F6]'
156183
>
157184
<div aria-hidden='true' className='absolute top-0 left-0 w-full'>
158185
<Image
@@ -166,7 +193,7 @@ export default function Features() {
166193
</div>
167194

168195
<div className='relative z-10 pt-[100px]'>
169-
<div className='flex flex-col items-start gap-[20px] px-[80px]'>
196+
<div ref={sectionRef} className='flex flex-col items-start gap-[20px] px-[80px]'>
170197
<Badge
171198
variant='blue'
172199
size='md'
@@ -186,111 +213,104 @@ export default function Features() {
186213
id='features-heading'
187214
className='max-w-[900px] font-[430] font-season text-[#1C1C1C] text-[40px] leading-[110%] tracking-[-0.02em]'
188215
>
189-
Everything you need to build, deploy, and manage AI agents.{' '}
216+
{HEADING_LETTERS.map((char, i) => (
217+
<ScrollLetter key={i} scrollYProgress={scrollYProgress} charIndex={i}>
218+
{char}
219+
</ScrollLetter>
220+
))}
190221
<span className='text-[#1C1C1C]/40'>
191222
Design powerful workflows, connect your data, and monitor every run — all in one
192223
platform.
193224
</span>
194225
</h2>
195226
</div>
196227

197-
<div className='mt-[73px] flex h-[68px] overflow-hidden border border-[#E9E9E9]'>
198-
<DotGrid cols={10} rows={8} width={80} />
199-
200-
<div role='tablist' aria-label='Feature categories' className='flex flex-1'>
201-
{FEATURE_TABS.map((tab, index) => (
202-
<button
203-
key={tab.label}
204-
type='button'
205-
role='tab'
206-
aria-selected={index === activeTab}
207-
onClick={() => setActiveTab(index)}
208-
className='relative flex h-full flex-1 items-center justify-center border-[#E9E9E9] border-l font-medium font-season text-[#212121] text-[14px] uppercase'
209-
style={{ backgroundColor: index === activeTab ? '#FDFDFD' : '#F6F6F6' }}
210-
>
211-
{tab.label}
212-
{index === activeTab && (
213-
<div className='absolute right-0 bottom-0 left-0 flex h-[6px]'>
214-
{tab.segments.map(([opacity, width], i) => (
215-
<div
216-
key={i}
217-
className='h-full shrink-0'
218-
style={{
219-
width: `${width}%`,
220-
backgroundColor: tab.color,
221-
opacity,
222-
}}
223-
/>
224-
))}
225-
</div>
226-
)}
227-
</button>
228-
))}
229-
</div>
228+
<div className='relative mt-[73px] pb-[80px]'>
229+
<div
230+
aria-hidden='true'
231+
className='absolute top-0 bottom-0 left-[80px] z-20 w-px bg-[#E9E9E9]'
232+
/>
233+
<div
234+
aria-hidden='true'
235+
className='absolute top-0 right-[80px] bottom-0 z-20 w-px bg-[#E9E9E9]'
236+
/>
230237

231-
<DotGrid cols={10} rows={8} width={80} borderLeft />
232-
</div>
238+
<div className='flex h-[68px] overflow-hidden border border-[#E9E9E9]'>
239+
<DotGrid cols={10} rows={8} width={80} />
233240

234-
<div className='mt-[60px] grid grid-cols-[1fr_2.8fr] gap-[60px] px-[80px]'>
235-
<div className='flex h-[560px] flex-col items-start justify-between pt-[20px]'>
236-
<div className='flex flex-col items-start gap-[16px]'>
237-
<h3 className='font-[430] font-season text-[#1C1C1C] text-[28px] leading-[120%] tracking-[-0.02em]'>
238-
{FEATURE_TABS[activeTab].title}
239-
</h3>
240-
<p className='font-[430] font-season text-[#1C1C1C]/50 text-[18px] leading-[150%] tracking-[0.02em]'>
241-
{FEATURE_TABS[activeTab].description}
242-
</p>
243-
</div>
244-
<Link
245-
href='/signup'
246-
className='group/cta inline-flex h-[32px] items-center gap-[6px] rounded-[5px] border border-[#1D1D1D] bg-[#1D1D1D] px-[10px] font-[430] font-season text-[14px] text-white transition-colors hover:border-[#2A2A2A] hover:bg-[#2A2A2A]'
247-
>
248-
{FEATURE_TABS[activeTab].cta}
249-
<span className='relative h-[10px] w-[10px] shrink-0'>
250-
<ChevronDown className='-rotate-90 absolute inset-0 h-[10px] w-[10px] transition-opacity duration-150 group-hover/cta:opacity-0' />
251-
<svg
252-
className='absolute inset-0 h-[10px] w-[10px] opacity-0 transition-opacity duration-150 group-hover/cta:opacity-100'
253-
viewBox='0 0 10 10'
254-
fill='none'
241+
<div role='tablist' aria-label='Feature categories' className='flex flex-1'>
242+
{FEATURE_TABS.map((tab, index) => (
243+
<button
244+
key={tab.label}
245+
type='button'
246+
role='tab'
247+
aria-selected={index === activeTab}
248+
onClick={() => setActiveTab(index)}
249+
className='relative flex h-full flex-1 items-center justify-center border-[#E9E9E9] border-l font-medium font-season text-[#212121] text-[14px] uppercase'
250+
style={{ backgroundColor: index === activeTab ? '#FDFDFD' : '#F6F6F6' }}
255251
>
256-
<path
257-
d='M1 5H8M5.5 2L8.5 5L5.5 8'
258-
stroke='currentColor'
259-
strokeWidth='1.33'
260-
strokeLinecap='square'
261-
strokeLinejoin='miter'
262-
fill='none'
263-
/>
264-
</svg>
265-
</span>
266-
</Link>
252+
{tab.label}
253+
{index === activeTab && (
254+
<div className='absolute right-0 bottom-0 left-0 flex h-[6px]'>
255+
{tab.segments.map(([opacity, width], i) => (
256+
<div
257+
key={i}
258+
className='h-full shrink-0'
259+
style={{
260+
width: `${width}%`,
261+
backgroundColor: tab.color,
262+
opacity,
263+
}}
264+
/>
265+
))}
266+
</div>
267+
)}
268+
</button>
269+
))}
270+
</div>
271+
272+
<DotGrid cols={10} rows={8} width={80} borderLeft />
267273
</div>
268274

269-
<div
270-
className='flex h-[560px] items-center justify-center rounded-[8px] border-2 border-dashed'
271-
style={{
272-
borderColor: hexToRgba(
273-
FEATURE_TABS[activeTab].badgeColor ?? FEATURE_TABS[activeTab].color,
274-
0.3
275-
),
276-
backgroundColor: hexToRgba(
277-
FEATURE_TABS[activeTab].badgeColor ?? FEATURE_TABS[activeTab].color,
278-
0.04
279-
),
280-
}}
281-
>
282-
<span
283-
className='font-[430] font-season text-[14px] uppercase tracking-[0.08em]'
284-
style={{
285-
color: hexToRgba(
286-
FEATURE_TABS[activeTab].badgeColor ?? FEATURE_TABS[activeTab].color,
287-
0.4
288-
),
289-
}}
290-
>
291-
{FEATURE_TABS[activeTab].label} preview
292-
</span>
275+
<div className='mt-[60px] grid grid-cols-[1fr_2.8fr] gap-[60px] px-[120px]'>
276+
<div className='flex h-[560px] flex-col items-start justify-between pt-[20px]'>
277+
<div className='flex flex-col items-start gap-[16px]'>
278+
<h3 className='font-[430] font-season text-[#1C1C1C] text-[28px] leading-[120%] tracking-[-0.02em]'>
279+
{FEATURE_TABS[activeTab].title}
280+
</h3>
281+
<p className='font-[430] font-season text-[#1C1C1C]/50 text-[18px] leading-[150%] tracking-[0.02em]'>
282+
{FEATURE_TABS[activeTab].description}
283+
</p>
284+
</div>
285+
<Link
286+
href='/signup'
287+
className='group/cta inline-flex h-[32px] items-center gap-[6px] rounded-[5px] border border-[#1D1D1D] bg-[#1D1D1D] px-[10px] font-[430] font-season text-[14px] text-white transition-colors hover:border-[#2A2A2A] hover:bg-[#2A2A2A]'
288+
>
289+
{FEATURE_TABS[activeTab].cta}
290+
<span className='relative h-[10px] w-[10px] shrink-0'>
291+
<ChevronDown className='-rotate-90 absolute inset-0 h-[10px] w-[10px] transition-opacity duration-150 group-hover/cta:opacity-0' />
292+
<svg
293+
className='absolute inset-0 h-[10px] w-[10px] opacity-0 transition-opacity duration-150 group-hover/cta:opacity-100'
294+
viewBox='0 0 10 10'
295+
fill='none'
296+
>
297+
<path
298+
d='M1 5H8M5.5 2L8.5 5L5.5 8'
299+
stroke='currentColor'
300+
strokeWidth='1.33'
301+
strokeLinecap='square'
302+
strokeLinejoin='miter'
303+
fill='none'
304+
/>
305+
</svg>
306+
</span>
307+
</Link>
308+
</div>
309+
310+
<FeaturesPreview />
293311
</div>
312+
313+
<div aria-hidden='true' className='mt-[60px] h-px bg-[#E9E9E9]' />
294314
</div>
295315
</div>
296316
</section>

0 commit comments

Comments
 (0)