chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import { ThinkingLoader } from '@/components/ui'
|
||||
import { WorkflowShowcase } from '@/app/(landing)/components/features/components/build-callout/components/workflow-showcase'
|
||||
|
||||
/**
|
||||
* The Build beat's callout - the finished workflow canvas centered on the
|
||||
* card's solid grey stage, with the goo cycle loader phasing through its
|
||||
* world-state phrases in the bottom-left corner. The Mothership chat loop
|
||||
* (`components/build-chat-animation`) is parked here unwired, kept for reuse
|
||||
* on another surface.
|
||||
*/
|
||||
export function BuildCallout() {
|
||||
return (
|
||||
<div aria-hidden='true' className='pointer-events-none absolute inset-0'>
|
||||
<WorkflowShowcase />
|
||||
<div className='absolute bottom-8 left-8 max-sm:bottom-4 max-sm:left-4'>
|
||||
<ThinkingLoader size={22} phase className='text-[var(--text-body)]' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+361
@@ -0,0 +1,361 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { cn } from '@sim/emcn'
|
||||
import { Blimp } from '@sim/emcn/icons'
|
||||
import { ArrowUp, Mic, Paperclip, Plus, Slash } from 'lucide-react'
|
||||
import { ThinkingLoader } from '@/components/ui'
|
||||
|
||||
const PROMPT = 'Build a workflow to schedule and publish posts to my X account.'
|
||||
const REPLY =
|
||||
'On it — building a workflow with a schedule trigger, a drafting agent, and an X publish step.'
|
||||
const REPLY_WORDS = REPLY.split(' ')
|
||||
|
||||
const TYPE_START_MS = 900
|
||||
const TYPE_CHAR_MS = 40
|
||||
const SEND_MS = 3800
|
||||
const THINKING_DONE_MS = 6000
|
||||
const REPLY_START_DELAY_MS = 420
|
||||
const REPLY_WORD_MS = 95
|
||||
const MERGE_MS = 9000
|
||||
const LOOP_MS = 10400
|
||||
|
||||
/** Resting gap between the chat surface and the composer once the split settles. */
|
||||
const SPLIT_GAP_PX = 10
|
||||
/** Goo blur decay window — the liquid neck between the boxes thins and snaps over this span. */
|
||||
const GOO_DECAY_MS = 850
|
||||
/** Peak feGaussianBlur stdDeviation at the send beat. */
|
||||
const GOO_MAX_BLUR = 16
|
||||
/** Blur decay tick; stepped so the blur eases out on a (1-t)^2 curve. */
|
||||
const GOO_STEP_MS = 40
|
||||
|
||||
/**
|
||||
* Chat-surface transition set while opening: geometry travels with the goo
|
||||
* split (500ms), the surface itself fades in fast (250ms), and the hairline
|
||||
* ring + elevation crisp in only after the liquid split settles (300ms fade,
|
||||
* 550ms delay).
|
||||
*/
|
||||
const CHAT_SURFACE_TRANSITION_OPEN =
|
||||
'[transition:height_500ms_ease-out,transform_500ms_ease-out,margin-bottom_500ms_ease-out,opacity_250ms_ease-out,box-shadow_300ms_ease-out_550ms]'
|
||||
|
||||
/**
|
||||
* Chat-surface transition set while merging back: same geometry travel, but
|
||||
* the ring + elevation drop immediately and the surface fades out fast so the
|
||||
* goo ghost carries the collapse into the composer.
|
||||
*/
|
||||
const CHAT_SURFACE_TRANSITION_MERGE =
|
||||
'[transition:height_500ms_ease-out,transform_500ms_ease-out,margin-bottom_500ms_ease-out,opacity_250ms_ease-out,box-shadow_200ms_ease-out]'
|
||||
|
||||
type BuildChatPhase =
|
||||
| 'idle'
|
||||
| 'typing'
|
||||
| 'thinking'
|
||||
| 'replyPreparing'
|
||||
| 'replying'
|
||||
| 'complete'
|
||||
| 'merging'
|
||||
|
||||
/**
|
||||
* Decorative Build-card Mothership loop. It mirrors the studio choreography
|
||||
* with local timers and CSS transitions so the landing page avoids motion
|
||||
* dependencies while preserving the product-window feel.
|
||||
*
|
||||
* The composer is a permanent fixture; the send beat gooey-morphs the chat
|
||||
* surface out of it, and the loop's final beat gooey-merges it back down in.
|
||||
* An SVG goo filter (state-driven blur → alpha-contrast matrix → atop
|
||||
* composite) is applied to an aria-hidden ghost layer of two solid white
|
||||
* rects mirroring the real boxes (both measured with a ResizeObserver) —
|
||||
* never to the real UI, which stays crisp above it. The filtered silhouette
|
||||
* renders a liquid meniscus that necks and snaps as the boxes separate (and
|
||||
* re-welds as they merge); each morph's blur decays on a (1-t)^2 curve over
|
||||
* 850ms. The chat surface's ring + shadow crisp in only after the split and
|
||||
* drop instantly at the merge, so the ghost carries the collapse.
|
||||
*/
|
||||
export function BuildChatAnimation() {
|
||||
const [phase, setPhase] = useState<BuildChatPhase>('idle')
|
||||
const [typedChars, setTypedChars] = useState(0)
|
||||
const [revealedWords, setRevealedWords] = useState(0)
|
||||
const [reducedMotion, setReducedMotion] = useState(false)
|
||||
const [morphing, setMorphing] = useState(false)
|
||||
const [gooBlur, setGooBlur] = useState(0)
|
||||
const [chatContentH, setChatContentH] = useState(0)
|
||||
const [composerH, setComposerH] = useState(94)
|
||||
const chatContentRef = useRef<HTMLDivElement>(null)
|
||||
const composerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const content = chatContentRef.current
|
||||
const composer = composerRef.current
|
||||
if (!content || !composer) return
|
||||
const measure = () => {
|
||||
setChatContentH(content.offsetHeight)
|
||||
setComposerH(composer.offsetHeight)
|
||||
}
|
||||
measure()
|
||||
const observer = new ResizeObserver(measure)
|
||||
observer.observe(content)
|
||||
observer.observe(composer)
|
||||
return () => observer.disconnect()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!morphing) return
|
||||
let progress = 0
|
||||
const interval = setInterval(() => {
|
||||
progress += GOO_STEP_MS / GOO_DECAY_MS
|
||||
if (progress >= 1) {
|
||||
setGooBlur(0)
|
||||
setMorphing(false)
|
||||
clearInterval(interval)
|
||||
return
|
||||
}
|
||||
setGooBlur(GOO_MAX_BLUR * (1 - progress) ** 2)
|
||||
}, GOO_STEP_MS)
|
||||
return () => clearInterval(interval)
|
||||
}, [morphing])
|
||||
|
||||
useEffect(() => {
|
||||
const media = window.matchMedia('(prefers-reduced-motion: reduce)')
|
||||
const timeouts: ReturnType<typeof setTimeout>[] = []
|
||||
const intervals: ReturnType<typeof setInterval>[] = []
|
||||
|
||||
const clearScheduled = () => {
|
||||
for (const timeout of timeouts) clearTimeout(timeout)
|
||||
for (const interval of intervals) clearInterval(interval)
|
||||
timeouts.length = 0
|
||||
intervals.length = 0
|
||||
}
|
||||
|
||||
const showFinished = () => {
|
||||
setReducedMotion(true)
|
||||
setPhase('complete')
|
||||
setTypedChars(0)
|
||||
setRevealedWords(REPLY_WORDS.length)
|
||||
setMorphing(false)
|
||||
setGooBlur(0)
|
||||
}
|
||||
|
||||
const startTyping = () => {
|
||||
setPhase('typing')
|
||||
const startedAt = performance.now()
|
||||
const interval = setInterval(() => {
|
||||
const elapsed = performance.now() - startedAt
|
||||
const nextChars = Math.min(Math.floor(elapsed / TYPE_CHAR_MS) + 1, PROMPT.length)
|
||||
setTypedChars(nextChars)
|
||||
if (nextChars >= PROMPT.length) clearInterval(interval)
|
||||
}, TYPE_CHAR_MS)
|
||||
intervals.push(interval)
|
||||
}
|
||||
|
||||
const startReply = () => {
|
||||
setPhase('replying')
|
||||
const startedAt = performance.now()
|
||||
const interval = setInterval(() => {
|
||||
const elapsed = performance.now() - startedAt
|
||||
const nextWords = Math.min(Math.floor(elapsed / REPLY_WORD_MS) + 1, REPLY_WORDS.length)
|
||||
setRevealedWords(nextWords)
|
||||
if (nextWords >= REPLY_WORDS.length) {
|
||||
clearInterval(interval)
|
||||
setPhase('complete')
|
||||
}
|
||||
}, REPLY_WORD_MS)
|
||||
intervals.push(interval)
|
||||
}
|
||||
|
||||
const runLoop = () => {
|
||||
clearScheduled()
|
||||
setReducedMotion(false)
|
||||
setPhase('idle')
|
||||
setTypedChars(0)
|
||||
setRevealedWords(0)
|
||||
setMorphing(false)
|
||||
setGooBlur(0)
|
||||
|
||||
timeouts.push(setTimeout(startTyping, TYPE_START_MS))
|
||||
timeouts.push(
|
||||
setTimeout(() => {
|
||||
setPhase('thinking')
|
||||
setTypedChars(0)
|
||||
setMorphing(true)
|
||||
setGooBlur(GOO_MAX_BLUR)
|
||||
}, SEND_MS)
|
||||
)
|
||||
timeouts.push(setTimeout(() => setPhase('replyPreparing'), THINKING_DONE_MS))
|
||||
timeouts.push(setTimeout(startReply, THINKING_DONE_MS + REPLY_START_DELAY_MS))
|
||||
timeouts.push(
|
||||
setTimeout(() => {
|
||||
setPhase('merging')
|
||||
setMorphing(true)
|
||||
setGooBlur(GOO_MAX_BLUR)
|
||||
}, MERGE_MS)
|
||||
)
|
||||
timeouts.push(setTimeout(runLoop, LOOP_MS))
|
||||
}
|
||||
|
||||
const syncMotionPreference = () => {
|
||||
clearScheduled()
|
||||
if (media.matches) {
|
||||
showFinished()
|
||||
return
|
||||
}
|
||||
runLoop()
|
||||
}
|
||||
|
||||
syncMotionPreference()
|
||||
media.addEventListener('change', syncMotionPreference)
|
||||
return () => {
|
||||
media.removeEventListener('change', syncMotionPreference)
|
||||
clearScheduled()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const composerText = phase === 'typing' ? PROMPT.slice(0, typedChars) : ''
|
||||
const chatOpen = reducedMotion || !['idle', 'typing', 'merging'].includes(phase)
|
||||
const thinkingPhraseVisible = phase === 'thinking'
|
||||
const replyVisible =
|
||||
reducedMotion || phase === 'replyPreparing' || phase === 'replying' || phase === 'complete'
|
||||
const replyComplete = reducedMotion || phase === 'complete'
|
||||
const replyText = REPLY_WORDS.slice(0, reducedMotion ? REPLY_WORDS.length : revealedWords).join(
|
||||
' '
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='absolute inset-0 flex items-end justify-start p-10 max-sm:px-3 max-sm:py-0 max-lg:items-center max-lg:justify-center max-lg:px-8 max-lg:py-8'>
|
||||
<svg width='0' height='0' aria-hidden='true' className='absolute'>
|
||||
<defs>
|
||||
<filter id='build-callout-goo'>
|
||||
<feGaussianBlur in='SourceGraphic' stdDeviation={gooBlur} result='blur' />
|
||||
<feColorMatrix
|
||||
in='blur'
|
||||
type='matrix'
|
||||
values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 19 -8'
|
||||
result='goo'
|
||||
/>
|
||||
<feComposite in='SourceGraphic' in2='goo' operator='atop' />
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<div className='relative h-[310px] w-full max-w-[560px] max-sm:h-[270px] max-sm:max-w-[430px]'>
|
||||
{!reducedMotion && (
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute inset-0 z-0 flex flex-col justify-end [filter:url(#build-callout-goo)_drop-shadow(0_1px_3px_rgba(28,40,64,0.10))]'
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'w-full rounded-2xl bg-[var(--white)] transition-[height,transform,margin-bottom] duration-500 ease-out motion-reduce:transition-none',
|
||||
chatOpen ? 'translate-y-0' : 'translate-y-5'
|
||||
)}
|
||||
style={{
|
||||
height: chatOpen ? chatContentH : 0,
|
||||
marginBottom: chatOpen ? SPLIT_GAP_PX : 0,
|
||||
}}
|
||||
/>
|
||||
<div className='w-full rounded-2xl bg-[var(--white)]' style={{ height: composerH }} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='relative z-10 flex h-full flex-col justify-end'>
|
||||
<section
|
||||
className={cn(
|
||||
'w-full overflow-hidden rounded-2xl bg-[var(--white)]',
|
||||
!reducedMotion &&
|
||||
(chatOpen ? CHAT_SURFACE_TRANSITION_OPEN : CHAT_SURFACE_TRANSITION_MERGE),
|
||||
chatOpen
|
||||
? 'translate-y-0 opacity-100 shadow-[0_24px_80px_color-mix(in_srgb,var(--text-primary)_14%,transparent),0_0_0_1px_var(--border-1)]'
|
||||
: 'translate-y-5 opacity-0 shadow-[0_24px_80px_transparent,0_0_0_1px_transparent]'
|
||||
)}
|
||||
style={{
|
||||
height: chatOpen ? chatContentH : 0,
|
||||
marginBottom: chatOpen ? SPLIT_GAP_PX : 0,
|
||||
}}
|
||||
>
|
||||
<div ref={chatContentRef} className='flex flex-col gap-4 p-4 max-sm:gap-2.5 max-sm:p-3'>
|
||||
<div className='ml-auto max-w-[78%] rounded-lg bg-[var(--surface-5)] px-3 py-2 text-[15px] text-[var(--text-primary)] leading-[1.45] max-sm:max-w-[86%] max-sm:text-[12px] max-sm:leading-[1.35]'>
|
||||
{PROMPT}
|
||||
</div>
|
||||
|
||||
<div className='relative min-h-[34px]'>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute inset-0 flex items-center transition-opacity duration-300 ease-out motion-reduce:transition-none',
|
||||
thinkingPhraseVisible ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
>
|
||||
<ThinkingLoader
|
||||
size={20}
|
||||
startVariant='corners'
|
||||
phase
|
||||
labelRatio={0.66}
|
||||
className='text-[var(--text-body)]'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-start gap-2.5 text-[15px] text-[var(--text-primary)] leading-[1.5] transition-opacity duration-300 ease-out motion-reduce:transition-none max-sm:text-[12px] max-sm:leading-[1.4]',
|
||||
replyVisible ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
>
|
||||
<span className='relative mt-0.5 size-[20px] shrink-0'>
|
||||
<span
|
||||
className={cn(
|
||||
'absolute inset-0 flex items-center justify-center transition-opacity duration-200 ease-out motion-reduce:transition-none',
|
||||
replyComplete ? 'opacity-0' : 'opacity-100'
|
||||
)}
|
||||
>
|
||||
<ThinkingLoader size={20} startVariant='corners' />
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'absolute inset-0 flex items-center justify-center transition-opacity duration-200 ease-out motion-reduce:transition-none',
|
||||
replyComplete ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
>
|
||||
<Blimp className='size-[18px] text-[var(--text-icon)]' />
|
||||
</span>
|
||||
</span>
|
||||
<p className='flex-1'>{replyText}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div
|
||||
ref={composerRef}
|
||||
className='h-[94px] w-full rounded-2xl border border-[var(--border-1)] bg-[var(--white)] px-2.5 py-2 shadow-[0_18px_60px_color-mix(in_srgb,var(--text-primary)_12%,transparent)]'
|
||||
>
|
||||
<p
|
||||
className={cn(
|
||||
'min-h-[42px] px-1.5 pt-1 text-[15px] leading-[1.35] max-sm:text-[13px]',
|
||||
composerText ? 'text-[var(--text-primary)]' : 'text-[var(--text-muted)]'
|
||||
)}
|
||||
>
|
||||
{composerText || 'Send message to Sim'}
|
||||
</p>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<span className='flex size-[28px] items-center justify-center rounded-full'>
|
||||
<Plus className='size-[16px] text-[var(--text-icon)]' />
|
||||
</span>
|
||||
<span className='flex size-[28px] items-center justify-center rounded-full'>
|
||||
<Paperclip className='size-[16px] text-[var(--text-icon)]' />
|
||||
</span>
|
||||
<span className='flex size-[28px] items-center justify-center rounded-full'>
|
||||
<Slash className='size-[16px] text-[var(--text-icon)]' />
|
||||
</span>
|
||||
<span className='ml-auto flex items-center gap-1.5'>
|
||||
<span className='flex size-[28px] items-center justify-center rounded-full'>
|
||||
<Mic className='size-[16px] text-[var(--text-icon)]' />
|
||||
</span>
|
||||
<span className='flex size-[28px] items-center justify-center rounded-full bg-[#808080]'>
|
||||
<ArrowUp className='size-[16px] text-[var(--surface-1)]' />
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { BuildChatAnimation } from './build-chat-animation'
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { WorkflowShowcase } from './workflow-showcase'
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
import {
|
||||
AgentIcon,
|
||||
AnthropicIcon,
|
||||
GmailIcon,
|
||||
LinearIcon,
|
||||
SlackIcon,
|
||||
TableIcon,
|
||||
} from '@/components/icons'
|
||||
import type { BlockDef } from '@/app/(landing)/components/hero/components/hero-visual/workflow-data'
|
||||
|
||||
/**
|
||||
* Design-space geometry for the Build card's workflow showcase - a
|
||||
* support-triage pipeline flowing LEFT TO RIGHT: two triggers (a Gmail inbox
|
||||
* and a Slack channel) converge on a triage agent, which fans out to four
|
||||
* destinations - Linear, an eng escalation, a customer reply, and Tables.
|
||||
* Rows carry concrete values (not placeholders) so the canvas reads like a
|
||||
* configured workflow.
|
||||
*
|
||||
* Column geometry: three columns at x=0 / x=370 / x=740 on a 990-wide canvas.
|
||||
* The triggers straddle the agent's row; the four outputs stack down the
|
||||
* right column, spanning y=20-666 on a 686-tall canvas. The bounding box
|
||||
* exactly hugs the blocks, so the stage's flex-centering shows the whole
|
||||
* flow clean and uncut. Edges run from right handles to left handles at
|
||||
* `HANDLE_Y_OFFSET`, matching the real editor's horizontal layout.
|
||||
*
|
||||
* Icon tiles follow the platform rule: grey text-ramp tiles for first-party
|
||||
* blocks, brand colors only for REAL third-party marks, and white bordered
|
||||
* tiles for marks that carry their own colors.
|
||||
*/
|
||||
export const SHOWCASE_BLOCKS: BlockDef[] = [
|
||||
{
|
||||
id: 'gmail-trigger',
|
||||
name: 'New support email',
|
||||
icon: GmailIcon,
|
||||
bgColor: '#FFFFFF',
|
||||
tileBorder: true,
|
||||
isTrigger: true,
|
||||
rows: [
|
||||
{ title: 'From', value: 'Customers' },
|
||||
{ title: 'Filter', value: 'Unread' },
|
||||
],
|
||||
x: 0,
|
||||
y: 150,
|
||||
},
|
||||
{
|
||||
id: 'slack-trigger',
|
||||
name: 'New #support post',
|
||||
icon: SlackIcon,
|
||||
bgColor: '#611F69',
|
||||
isTrigger: true,
|
||||
rows: [
|
||||
{ title: 'Channel', value: '#support' },
|
||||
{ title: 'Event', value: 'New message' },
|
||||
],
|
||||
x: 0,
|
||||
y: 424,
|
||||
},
|
||||
{
|
||||
id: 'triage',
|
||||
name: 'Triage request',
|
||||
icon: AgentIcon,
|
||||
bgColor: 'var(--text-primary)',
|
||||
rows: [
|
||||
{ title: 'Model', value: 'Claude', valueIcon: AnthropicIcon },
|
||||
{ title: 'Knowledge', value: 'Help center' },
|
||||
{ title: 'Instructions', value: 'Triage + draft' },
|
||||
],
|
||||
x: 370,
|
||||
y: 277,
|
||||
},
|
||||
{
|
||||
id: 'linear',
|
||||
name: 'File bug',
|
||||
icon: LinearIcon,
|
||||
bgColor: '#FFFFFF',
|
||||
tileBorder: true,
|
||||
isTerminal: true,
|
||||
rows: [
|
||||
{ title: 'Team', value: 'Platform' },
|
||||
{ title: 'Priority', value: 'From triage' },
|
||||
],
|
||||
x: 740,
|
||||
y: 20,
|
||||
},
|
||||
{
|
||||
id: 'escalate',
|
||||
name: 'Escalate to eng',
|
||||
icon: SlackIcon,
|
||||
bgColor: '#611F69',
|
||||
isTerminal: true,
|
||||
rows: [
|
||||
{ title: 'Channel', value: '#eng-oncall' },
|
||||
{ title: 'When', value: 'Urgent' },
|
||||
],
|
||||
x: 740,
|
||||
y: 200,
|
||||
},
|
||||
{
|
||||
id: 'reply',
|
||||
name: 'Send reply',
|
||||
icon: GmailIcon,
|
||||
bgColor: '#FFFFFF',
|
||||
tileBorder: true,
|
||||
isTerminal: true,
|
||||
rows: [
|
||||
{ title: 'To', value: 'Customer' },
|
||||
{ title: 'Tone', value: 'Friendly' },
|
||||
],
|
||||
x: 740,
|
||||
y: 380,
|
||||
},
|
||||
{
|
||||
id: 'tables',
|
||||
name: 'Log ticket',
|
||||
icon: TableIcon,
|
||||
bgColor: 'var(--text-body)',
|
||||
isTerminal: true,
|
||||
rows: [
|
||||
{ title: 'Table', value: 'Tickets' },
|
||||
{ title: 'Operation', value: 'Insert' },
|
||||
],
|
||||
x: 740,
|
||||
y: 560,
|
||||
},
|
||||
]
|
||||
|
||||
/** Source → target pairs; every edge is drawn (the flow renders finished). */
|
||||
export const SHOWCASE_EDGES: ReadonlyArray<readonly [string, string]> = [
|
||||
['gmail-trigger', 'triage'],
|
||||
['slack-trigger', 'triage'],
|
||||
['triage', 'linear'],
|
||||
['triage', 'escalate'],
|
||||
['triage', 'reply'],
|
||||
['triage', 'tables'],
|
||||
]
|
||||
|
||||
/** Design-space bounding box of the layout above. */
|
||||
export const SHOWCASE_CANVAS = { width: 990, height: 686 } as const
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import {
|
||||
SHOWCASE_BLOCKS,
|
||||
SHOWCASE_CANVAS,
|
||||
SHOWCASE_EDGES,
|
||||
} from '@/app/(landing)/components/features/components/build-callout/components/workflow-showcase/showcase-data'
|
||||
import { WorkflowBlock } from '@/app/(landing)/components/hero/components/hero-visual/workflow-block'
|
||||
import {
|
||||
BLOCK_WIDTH,
|
||||
HANDLE_Y_OFFSET,
|
||||
smoothStep,
|
||||
} from '@/app/(landing)/components/hero/components/hero-visual/workflow-data'
|
||||
|
||||
/**
|
||||
* The Build card's centerpiece - the left-to-right support-triage pipeline
|
||||
* from {@link SHOWCASE_BLOCKS} / {@link SHOWCASE_EDGES} rendered raw on the
|
||||
* card's solid grey stage, every block on canvas and every edge drawn. Static
|
||||
* and server-rendered; blocks reuse the hero-visual's {@link WorkflowBlock}
|
||||
* (the horizontal-flow card with left/right handle nubs).
|
||||
*
|
||||
* The 990x686 design canvas renders at fixed per-breakpoint scales - 0.62
|
||||
* (1280+), 0.38 (below `xl`, where the side-by-side card leaves the
|
||||
* aspect-locked stage narrow), 0.45 (below `lg`, stacked full-width), 0.29
|
||||
* (below `sm`) - each chosen as the largest zoom that keeps the WHOLE flow
|
||||
* inside that tier's media stage, so the graph sits centered and uncut. Each
|
||||
* sizer tier is the canvas dimensions times that tier's scale.
|
||||
*/
|
||||
export function WorkflowShowcase() {
|
||||
const byId = new Map(SHOWCASE_BLOCKS.map((block) => [block.id, block]))
|
||||
return (
|
||||
<div className='absolute inset-0 flex items-center justify-center'>
|
||||
<div className='relative h-[425px] w-[614px] shrink-0 max-sm:h-[199px] max-sm:w-[287px] max-lg:h-[309px] max-lg:w-[446px] max-xl:h-[261px] max-xl:w-[376px]'>
|
||||
<div className='absolute top-0 left-0 h-[686px] w-[990px] origin-top-left [transform:scale(0.62)] max-sm:[transform:scale(0.29)] max-lg:[transform:scale(0.45)] max-xl:[transform:scale(0.38)]'>
|
||||
<svg
|
||||
className='absolute inset-0 overflow-visible'
|
||||
width={SHOWCASE_CANVAS.width}
|
||||
height={SHOWCASE_CANVAS.height}
|
||||
viewBox={`0 0 ${SHOWCASE_CANVAS.width} ${SHOWCASE_CANVAS.height}`}
|
||||
fill='none'
|
||||
>
|
||||
{SHOWCASE_EDGES.map(([from, to]) => {
|
||||
const source = byId.get(from)
|
||||
const target = byId.get(to)
|
||||
if (!source || !target) return null
|
||||
return (
|
||||
<path
|
||||
key={`${from}-${to}`}
|
||||
d={smoothStep(
|
||||
source.x + BLOCK_WIDTH,
|
||||
source.y + HANDLE_Y_OFFSET,
|
||||
target.x,
|
||||
target.y + HANDLE_Y_OFFSET
|
||||
)}
|
||||
stroke='var(--workflow-edge)'
|
||||
strokeWidth={2}
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</svg>
|
||||
{SHOWCASE_BLOCKS.map((block) => (
|
||||
<div
|
||||
key={block.id}
|
||||
className='absolute'
|
||||
style={{ left: block.x, top: block.y, width: BLOCK_WIDTH }}
|
||||
>
|
||||
<WorkflowBlock block={block} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { BuildCallout } from './build-callout'
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { cn } from '@sim/emcn'
|
||||
|
||||
const CALLOUT_FADE =
|
||||
'[-webkit-mask-image:linear-gradient(to_bottom,#000_72%,transparent)] [mask-image:linear-gradient(to_bottom,#000_72%,transparent)]'
|
||||
|
||||
interface CalloutFrameProps {
|
||||
/** Width/layout for the panel (e.g. `w-[340px]`). */
|
||||
className?: string
|
||||
/** Sizing for the inner body (e.g. `h-[300px]`). */
|
||||
bodyClassName?: string
|
||||
/** Dissolve the body's lower edge so the surface reads as continuing below. */
|
||||
fade?: boolean
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* The shared chrome for a callout: an elevated panel that lifts a real Sim UI
|
||||
* surface off the backdrop, wearing the hero platform window's exact chrome -
|
||||
* 10px radius, `--surface-1` fill, and the hairline-ring + layered soft shadow
|
||||
* - so every floating window on the page reads as one family. Optionally fades
|
||||
* its body's foot so a long surface dissolves rather than ends on a hard edge.
|
||||
*/
|
||||
export function CalloutFrame({ className, bodyClassName, fade, children }: CalloutFrameProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'overflow-hidden rounded-[10px] bg-[var(--surface-1)] shadow-[0_0_0_1px_rgba(0,0,0,0.08),0_2px_6px_0_rgba(0,0,0,0.05),0_4px_42px_0_rgba(0,0,0,0.06)]',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className={cn('relative overflow-hidden', fade && CALLOUT_FADE, bodyClassName)}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { CalloutFrame } from './callout-frame'
|
||||
@@ -0,0 +1,128 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { ChipTag, cn } from '@sim/emcn'
|
||||
import { ArrowRight } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
|
||||
interface FeatureCardProps {
|
||||
/** Capability name shown as a chip tag pinned to the card's top-right corner. */
|
||||
eyebrow: string
|
||||
/** The beat's headline (`<h3>` - the section owns the single `<h2>`). */
|
||||
title: string
|
||||
/** Supporting copy beneath the headline. */
|
||||
description: string
|
||||
/** Optional trailing link (e.g. the feature's platform page). */
|
||||
href?: string
|
||||
/** Label for {@link href}. */
|
||||
linkLabel?: string
|
||||
/**
|
||||
* Backdrop image under the floating callout (public path). Omit for a solid
|
||||
* stage in the hero visual's light grey (`--surface-3`).
|
||||
*/
|
||||
backdropSrc?: string
|
||||
/**
|
||||
* Which side the media stage sits on from `lg` up (cards alternate down the
|
||||
* section, Cursor-style). Below `lg` the card always stacks media-first.
|
||||
*/
|
||||
mediaSide?: 'left' | 'right'
|
||||
/**
|
||||
* Square the card's bottom corners so its bottom edge merges with a
|
||||
* full-bleed divider drawn at the same line (the section's last card).
|
||||
*/
|
||||
flushBottom?: boolean
|
||||
/** The elevated real-UI callout floating over the backdrop. */
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Cursor-style feature card - one large OUTLINED container (a light
|
||||
* `--border` hairline on a transparent ground, no grey fill) holding a media
|
||||
* stage (a painted backdrop with the beat's real-UI callout floating over it)
|
||||
* and a vertically-centered copy column: `<h3>`, muted description, and an
|
||||
* optional arrow link. `mediaSide` picks which side the media sits on so the
|
||||
* cards can alternate down the section. The beat's name sits as a borderless
|
||||
* grey-filled {@link ChipTag} pinned to the card's top corner on the COPY
|
||||
* side (top-right when media is left, top-left when media is right), just
|
||||
* inside the outline - never floating over the media image.
|
||||
*
|
||||
* Below `lg` the card stacks - media on top, copy beneath - and the media
|
||||
* shortens so the card stays scannable in the compact grid. The pinned chip
|
||||
* would overlap the full-width media there, so it relocates INTO the copy
|
||||
* column, sitting above the `<h3>` as an in-flow eyebrow.
|
||||
*/
|
||||
export function FeatureCard({
|
||||
eyebrow,
|
||||
title,
|
||||
description,
|
||||
href,
|
||||
linkLabel,
|
||||
backdropSrc,
|
||||
mediaSide = 'left',
|
||||
flushBottom = false,
|
||||
children,
|
||||
}: FeatureCardProps) {
|
||||
const mediaRight = mediaSide === 'right'
|
||||
return (
|
||||
<article
|
||||
className={cn(
|
||||
'relative grid gap-10 rounded-[10px] border border-[var(--border)] p-4 max-lg:grid-cols-1 max-lg:gap-6',
|
||||
mediaRight ? 'grid-cols-[386px_1fr]' : 'grid-cols-[1fr_386px]',
|
||||
flushBottom && 'rounded-b-none'
|
||||
)}
|
||||
>
|
||||
<ChipTag
|
||||
variant='mono'
|
||||
className={cn('absolute top-4 z-10 max-lg:hidden', mediaRight ? 'left-4' : 'right-4')}
|
||||
>
|
||||
{eyebrow}
|
||||
</ChipTag>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className={cn(
|
||||
'relative aspect-[3/2] overflow-hidden rounded-[4px] max-lg:order-1 max-lg:aspect-[4/3]',
|
||||
!backdropSrc && 'bg-[var(--surface-3)]',
|
||||
mediaRight && 'lg:order-2'
|
||||
)}
|
||||
>
|
||||
{backdropSrc && (
|
||||
<Image
|
||||
src={backdropSrc}
|
||||
alt=''
|
||||
fill
|
||||
sizes='(max-width: 1460px) 70vw, 900px'
|
||||
className='object-cover'
|
||||
/>
|
||||
)}
|
||||
<div className='absolute inset-0 flex items-center justify-center p-4 [&>*]:max-w-full'>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col justify-center max-lg:order-2 max-lg:pb-2',
|
||||
mediaRight ? 'pl-4 max-lg:pl-0' : 'pr-4 max-lg:pr-0'
|
||||
)}
|
||||
>
|
||||
<ChipTag variant='mono' className='hidden max-lg:mb-3 max-lg:inline-flex max-lg:self-start'>
|
||||
{eyebrow}
|
||||
</ChipTag>
|
||||
<h3 className='text-balance font-medium text-[22px] text-[var(--text-primary)] leading-[1.3] max-sm:text-[20px]'>
|
||||
{title}
|
||||
</h3>
|
||||
<p className='mt-3 text-pretty text-[15px] text-[var(--text-muted)] leading-[1.6]'>
|
||||
{description}
|
||||
</p>
|
||||
{href && linkLabel && (
|
||||
<Link
|
||||
href={href}
|
||||
className='mt-5 flex items-center gap-1.5 text-[15px] text-[var(--text-body)] transition-colors hover-hover:text-[var(--text-primary)]'
|
||||
>
|
||||
{linkLabel}
|
||||
<ArrowRight className='size-[15px]' />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { FeatureCard } from './feature-card'
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import Image from 'next/image'
|
||||
import { CalloutFrame } from '@/app/(landing)/components/features/components/callout-frame'
|
||||
|
||||
/**
|
||||
* The Integrate beat's callout - the REAL platform Integrations page as one
|
||||
* floating window: a full capture of the workspace UI (sidebar + Integrations
|
||||
* tab with the showcase mosaic, search, and Featured sections) taken by
|
||||
* `exports/readme-banner/capture-integrations-ui.mjs` at the hero shot's card
|
||||
* geometry (1280x735 @2x), framed by the shared {@link CalloutFrame} so it
|
||||
* wears the hero platform window's exact chrome (10px radius + layered
|
||||
* shadow).
|
||||
*
|
||||
* The window is oversized (125% of the media stage, ~82% of the capture's
|
||||
* native scale) and anchored with visually EQUAL top and left insets
|
||||
* (percentage-based - 9.6% of the stage width; 14.4% of its 3:2 height lands
|
||||
* at the same px), so its top-left corner floats free over the backdrop while
|
||||
* the right AND bottom edges bleed past the media stage's clip - a zoomed-in
|
||||
* peek at part of the product rather than a complete miniature, scaling
|
||||
* proportionally with the aspect-locked stage. Decorative.
|
||||
*
|
||||
* `sizes` is derived directly from the section's grid math rather than
|
||||
* approximated, then rounded up to the worst-case (peak render/viewport
|
||||
* ratio) in each tier so the browser never under-fetches:
|
||||
* `callout = 1.25 * (viewport - 2*gutter - 32px card padding - [40px gap +
|
||||
* 386px fixed copy column, desktop only])`, gutter = `px-20`/`max-lg:px-8`/
|
||||
* `max-sm:px-5` from `Features`'s grid, matching `FeatureCard`'s
|
||||
* `max-lg:grid-cols-1` stack. Peak ratios (verified against a static
|
||||
* reproduction of this exact layout rendered at each Tailwind breakpoint):
|
||||
* ~113.3% at the `max-width: 1023px` stacked tier's own upper edge, ~108.6%
|
||||
* at `1460px` (the container's cap, where render width stops growing with
|
||||
* viewport - hence the final tier is a flat px value, not a vw fraction).
|
||||
*/
|
||||
export function IntegrationsCallout() {
|
||||
return (
|
||||
<div className='absolute inset-0'>
|
||||
<CalloutFrame
|
||||
className='absolute top-[14.4%] left-[9.6%] w-[125%]'
|
||||
bodyClassName='aspect-[1280/735]'
|
||||
>
|
||||
<Image
|
||||
src='/landing/feature-integrate-ui.png'
|
||||
alt=''
|
||||
fill
|
||||
sizes='(max-width: 1023px) 114vw, (max-width: 1460px) 109vw, 1053px'
|
||||
className='object-cover'
|
||||
/>
|
||||
</CalloutFrame>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import Image from 'next/image'
|
||||
import { CalloutFrame } from '@/app/(landing)/components/features/components/callout-frame'
|
||||
|
||||
/**
|
||||
* The Context beat's callout - the REAL platform Knowledge base page as one
|
||||
* floating window: a full capture of the workspace UI (sidebar with Knowledge
|
||||
* base selected + the knowledge list) taken by
|
||||
* `exports/readme-banner/capture-knowledge-ui.mjs` at the hero shot's card
|
||||
* geometry (1280x735 @2x), framed by the shared {@link CalloutFrame} so it
|
||||
* wears the hero platform window's exact chrome (10px radius + layered
|
||||
* shadow).
|
||||
*
|
||||
* Same oversized treatment as the Integrate card: 125% of the media stage
|
||||
* with EQUAL top and left insets (96px), so the top-left corner floats free
|
||||
* over the backdrop while the right and bottom edges bleed past the media
|
||||
* stage's clip. Decorative.
|
||||
*/
|
||||
export function KnowledgeCallout() {
|
||||
return (
|
||||
<div className='absolute inset-0'>
|
||||
<CalloutFrame
|
||||
className='absolute top-[14.4%] left-[9.6%] w-[125%]'
|
||||
bodyClassName='aspect-[1280/735]'
|
||||
>
|
||||
<Image
|
||||
src='/landing/feature-context-ui.png'
|
||||
alt=''
|
||||
fill
|
||||
sizes='1050px'
|
||||
className='object-cover'
|
||||
/>
|
||||
</CalloutFrame>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { LogsCallout } from './logs-callout'
|
||||
@@ -0,0 +1,35 @@
|
||||
import Image from 'next/image'
|
||||
import { CalloutFrame } from '@/app/(landing)/components/features/components/callout-frame'
|
||||
|
||||
/**
|
||||
* The Monitor beat's callout - the REAL platform Logs page as one floating
|
||||
* window: a full capture of the workspace UI (sidebar with Logs selected +
|
||||
* the seeded run table) taken by
|
||||
* `exports/readme-banner/capture-logs-ui.mjs` at the hero shot's card
|
||||
* geometry (1280x735 @2x), framed by the shared {@link CalloutFrame} so it
|
||||
* wears the hero platform window's exact chrome (10px radius + layered
|
||||
* shadow).
|
||||
*
|
||||
* Same oversized treatment as the Context card: 125% of the media stage with
|
||||
* EQUAL top and left insets (96px), so the top-left corner floats free over
|
||||
* the backdrop while the right and bottom edges bleed past the media stage's
|
||||
* clip. Decorative.
|
||||
*/
|
||||
export function LogsCallout() {
|
||||
return (
|
||||
<div className='absolute inset-0'>
|
||||
<CalloutFrame
|
||||
className='absolute top-[14.4%] left-[9.6%] w-[125%]'
|
||||
bodyClassName='aspect-[1280/735]'
|
||||
>
|
||||
<Image
|
||||
src='/landing/feature-monitor-ui.png'
|
||||
alt=''
|
||||
fill
|
||||
sizes='1050px'
|
||||
className='object-cover'
|
||||
/>
|
||||
</CalloutFrame>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { BuildCallout } from '@/app/(landing)/components/features/components/build-callout/build-callout'
|
||||
import { FeatureCard } from '@/app/(landing)/components/features/components/feature-card'
|
||||
import { IntegrationsCallout } from '@/app/(landing)/components/features/components/integrations-callout/integrations-callout'
|
||||
import { KnowledgeCallout } from '@/app/(landing)/components/features/components/knowledge-callout/knowledge-callout'
|
||||
import { LogsCallout } from '@/app/(landing)/components/features/components/logs-callout'
|
||||
|
||||
/**
|
||||
* Landing features - how Sim works, as a platform lifecycle. Four beats, in the
|
||||
* order you actually use Sim: bring your tools in (Integrate), give it data to
|
||||
* reason over (Context), build the agent logic (Build), then watch it run
|
||||
* (Monitor). Each beat is a Cursor-style {@link FeatureCard}: one large
|
||||
* outlined card holding a media stage (backdrop painting + elevated real-UI
|
||||
* callout) and a copy column, with the media side alternating card to card.
|
||||
*
|
||||
* The section's `<h2>` is `sr-only` - each beat carries its own visible `<h3>`,
|
||||
* so the section heading exists only to anchor the heading hierarchy and give AI
|
||||
* crawlers an atomic summary.
|
||||
*
|
||||
* Inter-section spacing is owned by the `<main>` flex `gap` in `landing.tsx`;
|
||||
* this section carries no vertical padding. The section itself is FULL-WIDTH so
|
||||
* its bottom rule can bleed to the browser edges; the card grid inside carries
|
||||
* the shared gutter (`px-20`) and the `max-w-[1460px]` cap. The last card
|
||||
* squares its bottom corners (`flushBottom`) and sits exactly on the rule, so
|
||||
* its outline merges into the full-bleed divider.
|
||||
*
|
||||
* The cards stack in a single column at every width on a 112px rhythm
|
||||
* (matching Cursor's spacing between feature cards). Below `lg` each card
|
||||
* internally reflows media-over-copy.
|
||||
*
|
||||
* Per-beat icons are still abstract placeholders (text eyebrows); distinct
|
||||
* abstract glyphs land in a later pass.
|
||||
*/
|
||||
export function Features() {
|
||||
return (
|
||||
<section id='features' aria-labelledby='features-heading' className='relative w-full'>
|
||||
<h2 id='features-heading' className='sr-only'>
|
||||
Integrate your tools, give Sim context, build agents, and monitor every run.
|
||||
</h2>
|
||||
|
||||
<div className='mx-auto grid w-full max-w-[1460px] grid-cols-1 gap-28 px-20 max-sm:gap-12 max-sm:px-5 max-lg:px-8'>
|
||||
{/* Integrate: bring your stack in. */}
|
||||
<FeatureCard
|
||||
eyebrow='Integrate'
|
||||
title='Connect the tools your work runs on.'
|
||||
description='Plug in 1,000+ integrations like Slack, HubSpot, Salesforce, and Notion, so Sim agents act across the stack you already use.'
|
||||
href='/integrations'
|
||||
linkLabel='Explore integrations'
|
||||
backdropSrc='/landing/feature-integrate-backdrop.jpg'
|
||||
>
|
||||
<IntegrationsCallout />
|
||||
</FeatureCard>
|
||||
|
||||
{/* Context: store data semantically. */}
|
||||
<FeatureCard
|
||||
eyebrow='Context'
|
||||
title='Give Sim data it can reason over.'
|
||||
description='Sim stores your data semantically in tables, files, and knowledge bases your agents read from to ground every answer in your own data.'
|
||||
backdropSrc='/landing/feature-context-backdrop.jpg'
|
||||
mediaSide='right'
|
||||
>
|
||||
<KnowledgeCallout />
|
||||
</FeatureCard>
|
||||
|
||||
{/* Build: wire agent logic in the visual builder. */}
|
||||
<FeatureCard
|
||||
eyebrow='Build'
|
||||
title='Build agents that solve real problems.'
|
||||
description='Wire blocks, models, and integrations into agent logic on a visual builder, from one agent to many working in parallel.'
|
||||
href='/workflows'
|
||||
linkLabel='Explore the workflow builder'
|
||||
>
|
||||
<BuildCallout />
|
||||
</FeatureCard>
|
||||
|
||||
{/* Monitor: watch every run. */}
|
||||
<FeatureCard
|
||||
eyebrow='Monitor'
|
||||
title='Watch every run, end to end.'
|
||||
description='Trace each run block by block, with full logs and the real cost, so you always know what ran and why.'
|
||||
backdropSrc='/landing/feature-monitor-backdrop.jpg'
|
||||
mediaSide='right'
|
||||
flushBottom
|
||||
>
|
||||
<LogsCallout />
|
||||
</FeatureCard>
|
||||
</div>
|
||||
|
||||
{/* Full-bleed rule the last card's squared bottom edge merges into -
|
||||
spans the whole browser, past the content cap and gutter (the section
|
||||
itself is full-width; only the card grid above is capped). */}
|
||||
<div aria-hidden='true' className='absolute inset-x-0 bottom-0 h-px bg-[var(--border)]' />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { Features } from './features'
|
||||
Reference in New Issue
Block a user