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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -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>
)
}
@@ -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>
)
}
@@ -0,0 +1 @@
export { BuildChatAnimation } from './build-chat-animation'
@@ -0,0 +1 @@
export { WorkflowShowcase } from './workflow-showcase'
@@ -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
@@ -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'
@@ -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>
)
}
@@ -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'