feat: integrations skeleton, realtime complete

This commit is contained in:
Emir Karabeg
2026-02-21 21:22:22 -08:00
parent bb3e899f74
commit 843af915bc
8 changed files with 545 additions and 374 deletions

View File

@@ -1,3 +1,155 @@
'use client'
import { useCallback, useEffect, useRef, useState } from 'react'
import Image from 'next/image'
import Link from 'next/link'
import { Badge, ChevronDown } from '@/components/emcn'
interface DotGridProps {
className?: string
cols: number
rows: number
gap?: number
}
function DotGrid({ className, cols, rows, gap = 0 }: DotGridProps) {
return (
<div
aria-hidden='true'
className={className}
style={{
display: 'grid',
gridTemplateColumns: `repeat(${cols}, 1fr)`,
gap,
placeItems: 'center',
}}
>
{Array.from({ length: cols * rows }, (_, i) => (
<div key={i} className='h-[2px] w-[2px] rounded-full bg-[#2A2A2A]' />
))}
</div>
)
}
const CURSOR_KEYFRAMES = `
@keyframes cursorVikhyath {
0% { transform: translate(0, 0); }
12% { transform: translate(120px, 10px); }
24% { transform: translate(80px, 80px); }
36% { transform: translate(-10px, 60px); }
48% { transform: translate(-15px, -20px); }
60% { transform: translate(100px, -40px); }
72% { transform: translate(180px, 30px); }
84% { transform: translate(50px, 50px); }
100% { transform: translate(0, 0); }
}
@keyframes cursorAlexa {
0% { transform: translate(0, 0); }
14% { transform: translate(45px, -35px); }
28% { transform: translate(-75px, 20px); }
42% { transform: translate(25px, -50px); }
57% { transform: translate(-65px, 15px); }
71% { transform: translate(35px, -30px); }
85% { transform: translate(-30px, -10px); }
100% { transform: translate(0, 0); }
}
@media (prefers-reduced-motion: reduce) {
@keyframes cursorVikhyath { 0%, 100% { transform: none; } }
@keyframes cursorAlexa { 0%, 100% { transform: none; } }
}
`
const CURSOR_ARROW_PATH =
'M17.135 2.198L12.978 14.821C12.478 16.339 10.275 16.16 10.028 14.581L9.106 8.703C9.01 8.092 8.554 7.599 7.952 7.457L1.591 5.953C0 5.577 0.039 3.299 1.642 2.978L15.39 0.229C16.534 0 17.499 1.09 17.135 2.198Z'
const CURSOR_ARROW_MIRRORED_PATH =
'M0.365 2.198L4.522 14.821C5.022 16.339 7.225 16.16 7.472 14.58L8.394 8.702C8.49 8.091 8.946 7.599 9.548 7.456L15.909 5.953C17.5 5.577 17.461 3.299 15.857 2.978L2.11 0.228C0.966 0 0.001 1.09 0.365 2.198Z'
function CursorArrow({ fill }: { fill: string }) {
return (
<svg width='23.15' height='21.1' viewBox='0 0 17.5 16.4' fill='none'>
<path d={fill === '#2ABBF8' ? CURSOR_ARROW_PATH : CURSOR_ARROW_MIRRORED_PATH} fill={fill} />
</svg>
)
}
function VikhyathCursor() {
return (
<div
aria-hidden='true'
className='pointer-events-none absolute'
style={{
top: '27.47%',
left: '25%',
animation: 'cursorVikhyath 16s ease-in-out infinite',
willChange: 'transform',
}}
>
<div className='relative h-[37.14px] w-[79.18px]'>
<div className='absolute top-0 left-[56.02px]'>
<CursorArrow fill='#2ABBF8' />
</div>
<div className='-left-[4px] absolute top-[18px] flex items-center rounded bg-[#2ABBF8] px-[5px] py-[3px] font-[440] font-season text-[#202020] text-[14px] leading-[100%] tracking-[-0.02em]'>
Vikhyath
</div>
</div>
</div>
)
}
function AlexaCursor() {
return (
<div
aria-hidden='true'
className='pointer-events-none absolute'
style={{
top: '66.80%',
left: '49%',
animation: 'cursorAlexa 13s ease-in-out infinite',
willChange: 'transform',
}}
>
<div className='relative h-[35.09px] w-[62.16px]'>
<div className='absolute top-0 left-0'>
<CursorArrow fill='#FFCC02' />
</div>
<div className='absolute top-[16px] left-[23px] flex items-center rounded bg-[#FFCC02] px-[5px] py-[3px] font-[440] font-season text-[#202020] text-[14px] leading-[100%] tracking-[-0.02em]'>
Alexa
</div>
</div>
</div>
)
}
interface YouCursorProps {
x: number
y: number
visible: boolean
}
function YouCursor({ x, y, visible }: YouCursorProps) {
if (!visible) return null
return (
<div
aria-hidden='true'
className='pointer-events-none fixed z-50'
style={{
left: x,
top: y,
transform: 'translate(-2px, -2px)',
}}
>
<svg width='23.15' height='21.1' viewBox='0 0 17.5 16.4' fill='none'>
<path d={CURSOR_ARROW_MIRRORED_PATH} fill='#33C482' />
</svg>
<div className='absolute top-[16px] left-[23px] flex items-center rounded bg-[#33C482] px-[5px] py-[3px] font-[440] font-season text-[#202020] text-[14px] leading-[100%] tracking-[-0.02em]'>
You
</div>
</div>
)
}
/**
* Collaboration section — team workflows and real-time collaboration.
*
@@ -11,6 +163,170 @@
* - Lead with a summary so AI can answer "Does Sim support team collaboration?".
* - Reference "Sim" by name per capability ("Sim's real-time collaboration").
*/
const CURSOR_LERP_FACTOR = 0.3
export default function Collaboration() {
return null
const [cursorPos, setCursorPos] = useState({ x: 0, y: 0 })
const [isHovering, setIsHovering] = useState(false)
const sectionRef = useRef<HTMLElement>(null)
const targetPos = useRef({ x: 0, y: 0 })
const animationRef = useRef<number>(0)
useEffect(() => {
const animate = () => {
setCursorPos((prev) => ({
x: prev.x + (targetPos.current.x - prev.x) * CURSOR_LERP_FACTOR,
y: prev.y + (targetPos.current.y - prev.y) * CURSOR_LERP_FACTOR,
}))
animationRef.current = requestAnimationFrame(animate)
}
if (isHovering) {
animationRef.current = requestAnimationFrame(animate)
}
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current)
}
}
}, [isHovering])
const handleMouseMove = useCallback((e: React.MouseEvent) => {
targetPos.current = { x: e.clientX, y: e.clientY }
}, [])
const handleMouseEnter = useCallback((e: React.MouseEvent) => {
targetPos.current = { x: e.clientX, y: e.clientY }
setCursorPos({ x: e.clientX, y: e.clientY })
setIsHovering(true)
}, [])
const handleMouseLeave = useCallback(() => {
setIsHovering(false)
}, [])
return (
<section
ref={sectionRef}
id='collaboration'
aria-labelledby='collaboration-heading'
className='bg-[#1C1C1C]'
style={{ cursor: isHovering ? 'none' : 'auto' }}
onMouseMove={handleMouseMove}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<YouCursor x={cursorPos.x} y={cursorPos.y} visible={isHovering} />
<style dangerouslySetInnerHTML={{ __html: CURSOR_KEYFRAMES }} />
<DotGrid
className='border-[#2A2A2A] border-y bg-[#1C1C1C] p-[6px]'
cols={120}
rows={1}
gap={6}
/>
<div className='relative overflow-hidden'>
<Link
href='/studio/multiplayer'
target='_blank'
rel='noopener noreferrer'
className='absolute bottom-10 left-4 z-20 flex cursor-none items-center gap-[14px] rounded-[5px] border border-[#2A2A2A] bg-[#1C1C1C] px-[12px] py-[10px] transition-colors hover:border-[#3d3d3d] hover:bg-[#232323] sm:left-8 md:left-[80px]'
>
<div className='relative h-7 w-11 shrink-0'>
<Image src='/landing/multiplayer-cursors.svg' alt='' fill className='object-contain' />
</div>
<div className='flex flex-col gap-[2px]'>
<span className='font-[430] font-season text-[#F6F6F0]/50 text-[12px] uppercase leading-[100%] tracking-[0.08em]'>
Blog
</span>
<span className='font-[430] font-season text-[#F6F6F0] text-[14px] leading-[125%] tracking-[0.02em]'>
How we built realtime collaboration
</span>
</div>
</Link>
<div className='grid grid-cols-[auto_1fr]'>
<div className='flex flex-col items-start gap-3 px-4 pt-[100px] pb-8 sm:gap-4 sm:px-8 md:gap-[20px] md:px-[80px]'>
<Badge
variant='blue'
size='md'
dot
className='bg-[#33C482]/10 font-season text-[#33C482] uppercase tracking-[0.02em]'
>
Teams
</Badge>
<h2
id='collaboration-heading'
className='font-[430] font-season text-[32px] text-white leading-[100%] tracking-[-0.02em] sm:text-[36px] md:text-[40px]'
>
Realtime
<br />
collaboration
</h2>
<p className='font-[430] font-season text-[#F6F6F0]/50 text-[14px] leading-[125%] tracking-[0.02em] sm:text-[16px]'>
Grab your team. Build agents together <br /> in real-time inside your workspace.
</p>
<Link
href='/signup'
className='group/cta mt-[12px] inline-flex h-[32px] cursor-none items-center gap-[6px] rounded-[5px] border border-[#33C482] bg-[#33C482] px-[10px] font-[430] font-season text-[14px] text-black transition-[filter] hover:brightness-110'
>
Build together
<span className='relative h-[10px] w-[10px] shrink-0'>
<ChevronDown className='-rotate-90 absolute inset-0 h-[10px] w-[10px] transition-opacity duration-150 group-hover/cta:opacity-0' />
<svg
className='absolute inset-0 h-[10px] w-[10px] opacity-0 transition-opacity duration-150 group-hover/cta:opacity-100'
viewBox='0 0 10 10'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M1 5H8M5.5 2L8.5 5L5.5 8'
stroke='currentColor'
strokeWidth='1.33'
strokeLinecap='square'
strokeLinejoin='miter'
fill='none'
/>
</svg>
</span>
</Link>
</div>
<figure className='pointer-events-none relative h-[600px] w-full'>
<div className='-left-[18%] absolute inset-y-0 min-w-full'>
<Image
src='/landing/collaboration-visual.svg'
alt='Collaboration visual showing team workflows with real-time editing, shared cursors, and version control interface'
width={876}
height={480}
className='h-full w-auto min-w-[100vw] object-left'
priority
/>
</div>
<div className='hidden lg:block'>
<VikhyathCursor />
<AlexaCursor />
</div>
<figcaption className='sr-only'>
Sim collaboration interface with real-time cursors, shared workspace, and team
presence indicators
</figcaption>
</figure>
</div>
</div>
<DotGrid
className='border-[#2A2A2A] border-y bg-[#1C1C1C] p-[6px]'
cols={120}
rows={1}
gap={6}
/>
</section>
)
}

View File

@@ -1,385 +1,42 @@
import { ArrowUp, AtSign, Paperclip } from 'lucide-react'
const HEADING = 'font-[550] font-season text-xl tracking-tight text-[#212121]'
const DESC =
'mt-4 max-w-[300px] font-[550] font-season text-sm leading-[125%] tracking-wide text-[#1C1C1C]/60'
const MODEL_ROWS = [
{
stagger: '19%',
models: [
{ name: 'Gemini 1.5 Pro', provider: 'Google' },
{ name: 'LLaMA 3', provider: 'Meta' },
{ name: 'Claude 3.5 Sonnet', provider: 'Anthropic' },
],
},
{
stagger: '29%',
models: [
{ name: 'GPT-4.1 / GPT-4o', provider: 'OpenAI' },
{ name: 'Mistral Large', provider: 'Mistral AI' },
{ name: 'Grok-2', provider: 'xAI' },
],
},
{
stagger: '9%',
models: [
{ name: 'Gemini 1.5 Pro', provider: 'Google' },
{ name: 'LLaMA 3', provider: 'Meta' },
{ name: 'Claude 3.5 Sonnet', provider: 'Anthropic' },
],
},
] as const
const COPILOT_ACTIONS = [
{ color: '#00F701', title: 'Build & edit workflows', subtitle: 'Help me build a workflow' },
{ color: '#F891E8', title: 'Debug workflows', subtitle: 'Help me debug my workflow' },
{ color: '#F04E9B', title: 'Optimize workflows', subtitle: 'help me optimize my workflow' },
] as const
const WORKFLOW_LOGS = [
{
name: 'main-relativity',
color: '#3367F5',
dots: [1, 2, 0, 2, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0],
},
{
name: 'readDocuments',
color: '#7632F5',
dots: [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
},
{
name: 'Escalation Triage...',
color: '#F218B9',
dots: [1, 2, 2, 2, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0],
},
{
name: 'acceptance/rejection',
color: '#F34E25',
dots: [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
},
] as const
const DOT_COLORS = ['#E0E0E0', '#00F701', '#FF6464'] as const
function ModelPill({ name, provider }: { name: string; provider: string }) {
return (
<div className='flex shrink-0 items-center gap-1 whitespace-nowrap rounded-full border border-[#D6D6D6] bg-white px-3 py-2 shadow-[0_1px_5px_rgba(0,0,0,0.05)]'>
<svg className='h-4 w-4 shrink-0 text-black/60' viewBox='0 0 16 16' fill='none'>
<path
d='M2 3.5h12M2 8h12M2 12.5h12'
stroke='currentColor'
strokeWidth='1.5'
strokeLinecap='round'
/>
</svg>
<span className='font-[350] text-black/80 text-sm'>{name}</span>
<span className='font-[350] text-black/40 text-sm'>{provider}</span>
</div>
)
}
function CopilotCell() {
return (
<div className='relative flex flex-[27] flex-col overflow-hidden'>
<div className='pointer-events-none absolute inset-x-0 bottom-0 h-1/2 rounded-t-lg border border-[#E9E9E9] bg-[#F3F3F3]' />
<div className='relative z-10 p-5'>
<h3 className={HEADING}>Co-pilot</h3>
<p className={DESC}>
Sim Copilot helps design, debug, and optimize workflows. It understands your setup, so
suggestions are relevant and actionable. From quick edits to deeper reasoning, Copilot
works alongside you at every step.
</p>
<p className='mt-5 font-[550] font-season text-[#1C1C1C] text-sm tracking-tight'>
Try it out
</p>
</div>
<div className='relative z-10 mt-auto px-4 pb-5' aria-hidden='true'>
<div className='absolute inset-x-2 top-[-18px] bottom-0 rounded-lg border border-[#EAEAEA] bg-white/80' />
<div className='relative flex flex-col gap-2.5'>
<div className='w-[150%] rounded-md border border-black/10 bg-white shadow-[0_1px_2px_#EBEBEB]'>
<div className='flex flex-col gap-3 p-2'>
<div className='flex h-6 w-6 items-center justify-center rounded bg-[#FFCC02]'>
<AtSign className='h-3.5 w-3.5 text-[#070707]' strokeWidth={2} />
</div>
<span className='font-[550] font-season text-base text-black/50 tracking-wide'>
Plan, search build anything
</span>
</div>
<div className='flex items-center justify-between p-2'>
<div className='flex items-center gap-1'>
<div className='flex h-6 items-center rounded bg-black/[0.04] px-1.5'>
<span className='font-[550] font-season text-black/60 text-sm'>Build</span>
</div>
<div className='flex h-6 items-center rounded bg-black/[0.04] px-1.5'>
<span className='font-[550] font-season text-black/60 text-sm'>claude..</span>
</div>
</div>
<div className='flex items-center gap-2.5'>
<Paperclip className='h-4 w-4 text-black/50' strokeWidth={1.5} />
<div className='flex h-6 w-6 items-center justify-center rounded-full bg-black'>
<ArrowUp className='h-3.5 w-3.5 text-white' strokeWidth={2.25} />
</div>
</div>
</div>
</div>
<div className='flex w-[150%] gap-2'>
{COPILOT_ACTIONS.map(({ color, title, subtitle }) => (
<div
key={title}
className='flex flex-1 flex-col gap-2 rounded-md border border-black/10 bg-white p-2 shadow-[0_1px_2px_#EBEBEB]'
>
<div className='h-5 w-5 rounded' style={{ backgroundColor: color }} />
<div className='flex flex-col gap-1'>
<span className='font-[550] font-season text-black/80 text-sm'>{title}</span>
<span className='font-[550] font-season text-black/50 text-xs'>{subtitle}</span>
</div>
</div>
))}
</div>
</div>
</div>
</div>
)
}
function ModelsCell() {
return (
<div className='relative flex-[70] overflow-hidden'>
<div className='relative z-10 p-5'>
<h3 className={HEADING}>Models</h3>
<p className={`${DESC} max-w-[440px]`}>
Sim Copilot helps design, debug, and optimize workflows. It understands your setup, so
suggestions are relevant and actionable.
</p>
</div>
<div className='relative z-10 mt-6' aria-hidden='true'>
{MODEL_ROWS.map((row, i) => (
<div key={i}>
<div className='border-[#EEE] border-t' />
<div className='flex items-center gap-8 py-2.5' style={{ paddingLeft: row.stagger }}>
{row.models.map((model, j) => (
<ModelPill key={j} name={model.name} provider={model.provider} />
))}
</div>
<div className='border-[#EEE] border-t' />
</div>
))}
</div>
<div
className='pointer-events-none absolute inset-y-0 left-0 z-20 w-1/4'
style={{ background: 'linear-gradient(90deg, #F6F6F6, transparent)' }}
/>
<div
className='pointer-events-none absolute inset-y-0 right-0 z-20 w-1/4'
style={{ background: 'linear-gradient(270deg, #F6F6F6, transparent)' }}
/>
</div>
)
}
function IntegrationsCell() {
return (
<div className='flex flex-[30] items-start overflow-hidden'>
<div className='shrink-0 p-5'>
<h3 className={HEADING}>Integrations</h3>
<p className={`${DESC} max-w-[220px]`}>
Sim works with the most popular tools &amp; applications with over 100+ integrations ready
to connect to your workflows instantly.
</p>
</div>
<div className='ml-auto flex flex-col gap-3 p-5' aria-hidden='true'>
<div className='mx-auto h-9 w-9 rounded border border-[#D6D6D6] bg-white shadow-[0_0_0_2px_#EAEAEA]' />
{[0, 1].map((row) => (
<div key={row} className='flex gap-5'>
{Array.from({ length: 5 }, (_, i) => (
<div
key={i}
className='h-9 w-9 rounded border border-[#D6D6D6] bg-white shadow-[0_0_0_2px_#EAEAEA]'
/>
))}
</div>
))}
<div className='flex gap-4 pl-6'>
{Array.from({ length: 4 }, (_, i) => (
<div key={i} className='h-12 w-11 rounded-sm border border-[#D5D5D5] border-dashed' />
))}
</div>
</div>
</div>
)
}
function DeployCell() {
return (
<div className='relative flex flex-1 flex-col overflow-hidden'>
<div className='pointer-events-none absolute inset-0 m-3 rounded-lg border border-[#E9E9E9] bg-[#F3F3F3]' />
<div className='relative z-10 p-5'>
<h3 className={HEADING}>Deploy / version</h3>
<p className={DESC}>
Sim Copilot helps design, debug, and optimize workflows. It understands your setup
</p>
</div>
<div className='relative z-10 mx-5 mt-auto mb-5' aria-hidden='true'>
<div className='max-w-[274px] rounded border border-[#D7D7D7] bg-white'>
<div className='flex items-center justify-between px-2.5 py-2'>
<div className='flex items-center gap-1.5'>
<div className='h-4 w-4 rounded bg-[#00F701]' />
<span className='font-[650] font-season text-[#1C1C1C] text-[9px]'>VERSION 1.2</span>
</div>
<div className='rounded-sm bg-[#00F701] px-1 py-0.5'>
<span className='font-[550] font-season text-[#1C1C1C] text-[9px]'>
[ INITIATE WF-001 ]
</span>
</div>
</div>
<div className='border-[#D7D7D7] border-t' />
<div className='flex flex-col gap-1 px-2.5 py-2'>
<span className='font-[550] font-season text-[#1C1C1C]/60 text-[9px]'>
Latest deployment: 12:0191:10198.00
</span>
<span className='font-[550] font-season text-[#1C1C1C]/30 text-[9px]'>
Deploy 08: 12:0191:10198.00
</span>
</div>
<div className='border-[#D7D7D7] border-t px-2.5 py-1.5'>
<span className='font-martian-mono font-medium text-[#414141] text-[5px] uppercase'>
TOTAL DEPLOYS: 09
</span>
</div>
</div>
</div>
</div>
)
}
function LogsCell() {
return (
<div className='relative flex flex-1 flex-col overflow-hidden'>
<div className='pointer-events-none absolute inset-0 m-3 rounded-lg border border-[#E9E9E9] bg-[#F3F3F3]' />
<div className='relative z-10 p-5 pb-0'>
<h3 className={HEADING}>Logs</h3>
<p className={DESC}>
Sim Copilot helps design, debug, and optimize workflows. It understands your setup
</p>
</div>
<div className='relative z-10 mx-1.5 mt-auto mb-1.5' aria-hidden='true'>
<div className='rounded border border-[#D7D7D7] bg-white'>
<div className='flex items-center gap-1.5 px-3 py-2.5'>
<div className='flex shrink-0 items-center justify-center rounded bg-[#FCB409] p-1'>
<svg className='h-[9px] w-[9px]' viewBox='0 0 14 14' fill='none'>
<circle cx='7' cy='4.5' r='2' stroke='#1C1C1C' strokeWidth='1.4' />
<path d='M7 7.5V10' stroke='#1C1C1C' strokeWidth='1.4' strokeLinecap='round' />
<circle cx='7' cy='12' r='1.5' stroke='#1C1C1C' strokeWidth='1.4' />
</svg>
</div>
<span className='font-[650] font-season text-[#1C1C1C] text-[10px]'>Logs</span>
</div>
<div className='border-[#D7D7D7]/50 border-t' />
<div className='flex flex-col gap-2 px-3 py-2.5'>
{WORKFLOW_LOGS.map(({ name, color, dots }) => (
<div key={name} className='flex items-center gap-2'>
<div className='h-2 w-2 shrink-0 rounded-sm' style={{ backgroundColor: color }} />
<span className='w-[72px] shrink-0 truncate font-[550] font-season text-[#777] text-[11px] tracking-tight'>
{name}
</span>
<div className='flex flex-1 gap-px'>
{dots.map((d, i) => (
<div
key={i}
className='h-3 flex-1 rounded-full'
style={{ backgroundColor: DOT_COLORS[d] }}
/>
))}
</div>
</div>
))}
</div>
</div>
</div>
</div>
)
}
import Image from 'next/image'
import { Badge } from '@/components/emcn'
export default function Features() {
return (
<section
id='features'
aria-labelledby='features-heading'
className='relative overflow-hidden bg-[#F6F6F6] pb-36'
className='relative overflow-hidden bg-[#F6F6F6] pb-[144px]'
>
{/* Dark transition from Templates section above */}
<div
aria-hidden='true'
className='absolute top-9 right-0 h-[200px] w-[72%] rounded-b-[9px] border border-[#3E3E3E] bg-[#212121]'
>
<div className='flex h-5 gap-px overflow-hidden'>
<div className='flex-[47] rounded bg-[#2ABBF8] opacity-80' />
<div className='flex-[22] rounded bg-[#00F701] opacity-40' />
<div className='flex-[24] rounded bg-[#00F701] opacity-80' />
<div className='flex-[62] rounded bg-[#2ABBF8] opacity-80' />
</div>
<div className='mt-px flex h-5 gap-px'>
<div className='flex-[27] rounded bg-[#010370]' />
<div className='flex-[24] rounded bg-[#FA4EDF]' />
<div className='flex-[13] rounded bg-[#0607EB]' />
<div className='flex-[27] rounded bg-[#010370]' />
<div className='flex-[24] rounded bg-[#FA4EDF]' />
</div>
<div aria-hidden='true' className='absolute top-0 left-0 w-full'>
<Image
src='/landing/features-transition.svg'
alt=''
width={1440}
height={366}
className='h-auto w-full'
priority
/>
</div>
{/* Header */}
<div className='relative z-10 px-20 pt-28'>
<div className='flex flex-col gap-5'>
<div className='inline-flex items-center gap-2 rounded bg-[#1C1C1C]/10 p-1'>
<div className='h-2 w-2 rounded-sm bg-[#1C1C1C]' />
<span className='font-martian-mono font-medium text-[#1C1C1C] text-xs uppercase tracking-[0.02em]'>
how sim works
</span>
</div>
<div className='relative z-10 px-[80px] pt-[100px]'>
<div className='flex flex-col items-start gap-[20px]'>
<Badge
variant='blue'
size='md'
dot
className='bg-[#FA4EDF]/10 font-season text-[#FA4EDF] uppercase tracking-[0.02em]'
>
Integrations
</Badge>
<h2
id='features-heading'
className='font-[550] font-season text-[#1C1C1C] text-[40px] leading-none tracking-tight'
className='font-[430] font-season text-[#1C1C1C] text-[40px] leading-[100%] tracking-[-0.02em]'
>
Product features
Everything you need
</h2>
</div>
</div>
{/* Feature Grid */}
<div className='relative z-10 mx-20 mt-12'>
<div className='flex overflow-hidden rounded-[9px] border-[#E9E9E9] border-[1.5px]'>
<CopilotCell />
<div className='w-7 shrink-0 border-[#E9E9E9] border-x bg-[#FDFDFD]' />
<div className='flex flex-[43] flex-col overflow-hidden'>
<ModelsCell />
<div className='h-7 shrink-0 border-[#E9E9E9] border-y bg-[#FDFDFD]' />
<IntegrationsCell />
</div>
<div className='w-7 shrink-0 border-[#E9E9E9] border-x bg-[#FDFDFD]' />
<div className='flex flex-[27] flex-col overflow-hidden'>
<DeployCell />
<div className='h-7 shrink-0 border-[#E9E9E9] border-y bg-[#FDFDFD]' />
<LogsCell />
</div>
</div>
</div>
</section>
)
}

View File

@@ -1,6 +1,7 @@
'use client'
import { useState } from 'react'
import { useRef, useState } from 'react'
import { type MotionValue, motion, useScroll, useTransform } from 'framer-motion'
import dynamic from 'next/dynamic'
import Link from 'next/link'
import { Badge, ChevronDown } from '@/components/emcn'
@@ -231,6 +232,73 @@ const DEPTH_CONFIGS: Record<string, DepthConfig> = {
},
}
const SCROLL_BLOCK_RX = '2.59574'
/**
* Two-row horizontal block strip for the scroll-driven reveal in the templates section.
* Same structural pattern as the hero's top-right blocks with matching colours:
* blue (left) → pink (middle) → green (right).
*/
const SCROLL_BLOCK_RECTS = [
{ opacity: 0.6, x: '-34.24', y: '0', width: '34.24', height: '16.86', fill: '#2ABBF8' },
{ opacity: 1, x: '-17.38', y: '0', width: '16.86', height: '16.86', fill: '#2ABBF8' },
{ opacity: 1, x: '0', y: '0', width: '16.86', height: '33.73', fill: '#2ABBF8' },
{ opacity: 0.6, x: '0', y: '0', width: '85.34', height: '16.86', fill: '#2ABBF8' },
{ opacity: 1, x: '0', y: '0', width: '16.86', height: '16.86', fill: '#2ABBF8' },
{ opacity: 0.6, x: '34.24', y: '0', width: '34.24', height: '33.73', fill: '#2ABBF8' },
{ opacity: 1, x: '34.24', y: '0', width: '16.86', height: '16.86', fill: '#2ABBF8' },
{ opacity: 1, x: '51.62', y: '16.86', width: '16.86', height: '16.86', fill: '#2ABBF8' },
{ opacity: 1, x: '68.48', y: '0', width: '54.65', height: '16.86', fill: '#FA4EDF' },
{ opacity: 0.6, x: '106.27', y: '0', width: '34.24', height: '33.73', fill: '#FA4EDF' },
{ opacity: 0.6, x: '106.27', y: '0', width: '51.10', height: '16.86', fill: '#FA4EDF' },
{ opacity: 1, x: '123.65', y: '16.86', width: '16.86', height: '16.86', fill: '#FA4EDF' },
{ opacity: 0.6, x: '157.37', y: '0', width: '34.24', height: '16.86', fill: '#FA4EDF' },
{ opacity: 1, x: '157.37', y: '0', width: '16.86', height: '16.86', fill: '#FA4EDF' },
{ opacity: 0.6, x: '209.0', y: '0', width: '68.48', height: '16.86', fill: '#00F701' },
{ opacity: 0.6, x: '209.14', y: '0', width: '16.86', height: '33.73', fill: '#00F701' },
{ opacity: 0.6, x: '243.23', y: '0', width: '34.24', height: '33.73', fill: '#00F701' },
{ opacity: 1, x: '243.23', y: '0', width: '16.86', height: '16.86', fill: '#00F701' },
{ opacity: 0.6, x: '260.10', y: '0', width: '34.04', height: '16.86', fill: '#00F701' },
{ opacity: 1, x: '260.61', y: '16.86', width: '16.86', height: '16.86', fill: '#00F701' },
] as const
const SCROLL_BLOCK_MAX_X = Math.max(...SCROLL_BLOCK_RECTS.map((r) => Number.parseFloat(r.x)))
const SCROLL_REVEAL_START = 0.05
const SCROLL_REVEAL_SPAN = 0.7
const SCROLL_FADE_IN = 0.03
function getScrollBlockThreshold(x: string): number {
const normalized = Number.parseFloat(x) / SCROLL_BLOCK_MAX_X
return SCROLL_REVEAL_START + (1 - normalized) * SCROLL_REVEAL_SPAN
}
interface ScrollBlockRectProps {
scrollYProgress: MotionValue<number>
rect: (typeof SCROLL_BLOCK_RECTS)[number]
}
/** Renders a single SVG rect whose opacity is driven by scroll progress. */
function ScrollBlockRect({ scrollYProgress, rect }: ScrollBlockRectProps) {
const threshold = getScrollBlockThreshold(rect.x)
const opacity = useTransform(
scrollYProgress,
[threshold, threshold + SCROLL_FADE_IN],
[0, rect.opacity]
)
return (
<motion.rect
x={rect.x}
y={rect.y}
width={rect.width}
height={rect.height}
rx={SCROLL_BLOCK_RX}
fill={rect.fill}
style={{ opacity }}
/>
)
}
function buildBottomWallStyle(config: DepthConfig) {
let pos = 0
const stops: string[] = []
@@ -274,13 +342,24 @@ function DotGrid({ className, cols, rows, gap = 0 }: DotGridProps) {
const TEMPLATES_PANEL_ID = 'templates-panel'
export default function Templates() {
const sectionRef = useRef<HTMLDivElement>(null)
const [activeIndex, setActiveIndex] = useState(0)
const { scrollYProgress } = useScroll({
target: sectionRef,
offset: ['start 0.9', 'start 0.2'],
})
const activeWorkflow = TEMPLATE_WORKFLOWS[activeIndex]
const activeDepth = DEPTH_CONFIGS[activeWorkflow.id]
return (
<section id='templates' aria-labelledby='templates-heading' className='mt-[40px] mb-[80px]'>
<section
ref={sectionRef}
id='templates'
aria-labelledby='templates-heading'
className='mt-[40px] mb-[80px]'
>
<p className='sr-only'>
Sim includes {TEMPLATE_WORKFLOWS.length} pre-built workflow templates covering OCR
processing, release management, meeting follow-ups, resume scanning, email triage,
@@ -298,6 +377,24 @@ export default function Templates() {
/>
<div className='relative overflow-hidden'>
<div
aria-hidden='true'
className='pointer-events-none absolute top-0 right-0 z-20 hidden lg:block'
>
<svg
width={329}
height={34}
viewBox='-34 0 329 34'
fill='none'
xmlns='http://www.w3.org/2000/svg'
className='h-auto w-full'
>
{SCROLL_BLOCK_RECTS.map((r, i) => (
<ScrollBlockRect key={i} scrollYProgress={scrollYProgress} rect={r} />
))}
</svg>
</div>
<div className='px-[80px] pt-[100px]'>
<div className='flex flex-col items-start gap-[20px]'>
<Badge
@@ -317,12 +414,12 @@ export default function Templates() {
id='templates-heading'
className='font-[430] font-season text-[40px] text-white leading-[100%] tracking-[-0.02em]'
>
Go from idea to production in minutes.
Ship your agent in minutes
</h2>
<p className='max-w-[640px] font-[430] font-season text-[#F6F6F0]/50 text-[16px] leading-[125%] tracking-[0.02em]'>
1,000+ integrations wired into pre-built templates for every use casepick one, swap
models and tools to fit your stack, and deploy in minutes.
<p className='font-[430] font-season text-[#F6F6F0]/50 text-[16px] leading-[125%] tracking-[0.02em]'>
Pre-built templates for every use casepick one, swap <br />
models and tools to fit your stack, and deploy.
</p>
</div>
</div>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 58 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 963 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 45 KiB