diff --git a/apps/sim/app/(landing)/CLAUDE.md b/apps/sim/app/(landing)/CLAUDE.md index 992e2172448..e7dd7fe7534 100644 --- a/apps/sim/app/(landing)/CLAUDE.md +++ b/apps/sim/app/(landing)/CLAUDE.md @@ -18,11 +18,11 @@ The landing page looks like the product. Its visual language is the workspace UI - **Never touch global styles.** No additions to `app/_styles/globals.css`. All styling is local Tailwind classes; `cn()` from `@/lib/core/utils/cn` for conditionals; no inline `style` attributes. - **Responsive - desktop is the source of truth, scaled down via `max-*` overrides.** The page is fully responsive (iPad + phone). The desktop layout stays the unprefixed baseline; smaller screens are handled by *layering* `max-*` overrides on top, so desktop renders byte-identically. Tiers: - `max-xl:` (≤1279) - the hero's two-panel split (absolute visual + logos) collapses to a stacked, in-flow column. The split needs ≥1280 to avoid the headline colliding with the visual panel; iPad-landscape (1024) therefore gets the stacked hero with the desktop nav. - - `max-lg:` (≤1023) - the desktop nav clusters hide (`hidden lg:flex`) and `MobileNav` (hamburger sheet) takes over; multi-column grids step down (mothership 4→2, footer 7→3); shared gutter `px-12 → max-lg:px-8`; section gaps tighten. + - `max-lg:` (≤1023) - the desktop nav clusters hide (`hidden lg:flex`) and `MobileNav` (hamburger sheet) takes over; multi-column grids step down (mothership 4→2, footer 7→3); shared gutter `px-20 → max-lg:px-8`; section gaps tighten. - `max-md:` (≤767) - Features beats drop the floating callout (`max-md:hidden`) and show the un-masked backdrop preview full-width. - `max-sm:` (≤639) - single-column grids, smallest type scale, `px-5` gutter, hero CTA row stacks. - When adding a new section, give it the same `px-12 max-lg:px-8 max-sm:px-5` gutter so the navbar wordmark stays aligned with section content at every width. Verify desktop is unchanged and there is zero horizontal overflow at 1280 / 1024 / 768 / 390 before shipping. + When adding a new section, give it the same `px-20 max-lg:px-8 max-sm:px-5` gutter so the navbar wordmark stays aligned with section content at every width. Verify desktop is unchanged and there is zero horizontal overflow at 1280 / 1024 / 768 / 390 before shipping. ## Performance - page speed is a feature diff --git a/apps/sim/app/(landing)/blog/[slug]/loading.tsx b/apps/sim/app/(landing)/blog/[slug]/loading.tsx index 01dead9d956..accf7788da3 100644 --- a/apps/sim/app/(landing)/blog/[slug]/loading.tsx +++ b/apps/sim/app/(landing)/blog/[slug]/loading.tsx @@ -3,7 +3,7 @@ import { Skeleton } from '@sim/emcn' export default function BlogPostLoading() { return (
-
+
@@ -31,8 +31,8 @@ export default function BlogPostLoading() {
-
-
+
+
diff --git a/apps/sim/app/(landing)/blog/[slug]/page.tsx b/apps/sim/app/(landing)/blog/[slug]/page.tsx index 42371752d9a..afd7f32a439 100644 --- a/apps/sim/app/(landing)/blog/[slug]/page.tsx +++ b/apps/sim/app/(landing)/blog/[slug]/page.tsx @@ -39,7 +39,7 @@ export default async function Page({ params }: { params: Promise<{ slug: string return (
-
+
@@ -118,7 +118,7 @@ export default async function Page({ params }: { params: Promise<{ slug: string
-
+
diff --git a/apps/sim/app/(landing)/blog/loading.tsx b/apps/sim/app/(landing)/blog/loading.tsx index 623fcafbbcf..f99f2c60c4a 100644 --- a/apps/sim/app/(landing)/blog/loading.tsx +++ b/apps/sim/app/(landing)/blog/loading.tsx @@ -3,9 +3,9 @@ import { Skeleton } from '@sim/emcn' export default function BlogLoading() { return (
-
+
{/* Header skeleton */} -
+
@@ -14,7 +14,7 @@ export default function BlogLoading() {
{/* Content area with vertical border rails */} -
+
{/* Featured skeleton */} diff --git a/apps/sim/app/(landing)/blog/page.tsx b/apps/sim/app/(landing)/blog/page.tsx index 54a0b72d65d..5d5c0d6172d 100644 --- a/apps/sim/app/(landing)/blog/page.tsx +++ b/apps/sim/app/(landing)/blog/page.tsx @@ -103,7 +103,7 @@ export default async function BlogIndex({ {/* Section header */} -
+

Latest from Sim @@ -118,7 +118,7 @@ export default async function BlogIndex({
{/* Content area with vertical border rails */} -
+
{/* Featured posts */} {featured.length > 0 && ( diff --git a/apps/sim/app/(landing)/careers/careers.tsx b/apps/sim/app/(landing)/careers/careers.tsx index b8c8de250d3..56c4d2fe194 100644 --- a/apps/sim/app/(landing)/careers/careers.tsx +++ b/apps/sim/app/(landing)/careers/careers.tsx @@ -21,8 +21,8 @@ interface CareersProps { * crawlable HTML; the interactive {@link JobBoard} hydrates on top to add * Team/Location filtering. * - * Both sections share the landing gutter — capped and centered at `max-w-[1446px]` - * with the navbar-aligned `px-12 max-lg:px-8 max-sm:px-5` so the headline starts on + * Both sections share the landing gutter — capped and centered at `max-w-[1460px]` + * with the navbar-aligned `px-20 max-lg:px-8 max-sm:px-5` so the headline starts on * the same vertical line as the wordmark. The hero carries the single `

` * (containing "Sim" and "AI workspace") plus an sr-only product summary for AI * citation (landing CLAUDE.md → GEO); the roles section owns its own `

`. @@ -45,7 +45,7 @@ export default async function Careers({ searchParams }: CareersProps) {

Careers at Sim, the open-source AI workspace where teams build, deploy, and manage AI @@ -70,7 +70,7 @@ export default async function Careers({ searchParams }: CareersProps) {

` exactly (48px / `leading-[1.1]` and the same responsive * ramp), so the page opens and closes on the same display size. Horizontal - * padding (`px-12`) matches every section above, and the section is capped and - * centered at the shared `max-w-[1446px]`. + * padding (`px-20`) matches every section above, and the section is capped and + * centered at the shared `max-w-[1460px]`. */ export function Cta() { return (

+ ) +} diff --git a/apps/sim/app/(landing)/components/features/components/build-callout/components/build-chat-animation/build-chat-animation.tsx b/apps/sim/app/(landing)/components/features/components/build-callout/components/build-chat-animation/build-chat-animation.tsx new file mode 100644 index 00000000000..1daf1d561c1 --- /dev/null +++ b/apps/sim/app/(landing)/components/features/components/build-callout/components/build-chat-animation/build-chat-animation.tsx @@ -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 '@/app/(landing)/components/thinking-loader' + +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('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(null) + const composerRef = useRef(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[] = [] + const intervals: ReturnType[] = [] + + 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 ( +
+ + +
+ {!reducedMotion && ( + + ) +} diff --git a/apps/sim/app/(landing)/components/features/components/build-callout/components/build-chat-animation/index.ts b/apps/sim/app/(landing)/components/features/components/build-callout/components/build-chat-animation/index.ts new file mode 100644 index 00000000000..cb36e136dec --- /dev/null +++ b/apps/sim/app/(landing)/components/features/components/build-callout/components/build-chat-animation/index.ts @@ -0,0 +1 @@ +export { BuildChatAnimation } from './build-chat-animation' diff --git a/apps/sim/app/(landing)/components/features/components/build-callout/components/workflow-showcase/index.ts b/apps/sim/app/(landing)/components/features/components/build-callout/components/workflow-showcase/index.ts new file mode 100644 index 00000000000..c6daec7509e --- /dev/null +++ b/apps/sim/app/(landing)/components/features/components/build-callout/components/workflow-showcase/index.ts @@ -0,0 +1 @@ +export { WorkflowShowcase } from './workflow-showcase' diff --git a/apps/sim/app/(landing)/components/features/components/build-callout/components/workflow-showcase/showcase-data.ts b/apps/sim/app/(landing)/components/features/components/build-callout/components/workflow-showcase/showcase-data.ts new file mode 100644 index 00000000000..81099f42ebc --- /dev/null +++ b/apps/sim/app/(landing)/components/features/components/build-callout/components/workflow-showcase/showcase-data.ts @@ -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 = [ + ['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 diff --git a/apps/sim/app/(landing)/components/features/components/build-callout/components/workflow-showcase/workflow-showcase.tsx b/apps/sim/app/(landing)/components/features/components/build-callout/components/workflow-showcase/workflow-showcase.tsx new file mode 100644 index 00000000000..7c6e239bb5d --- /dev/null +++ b/apps/sim/app/(landing)/components/features/components/build-callout/components/workflow-showcase/workflow-showcase.tsx @@ -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 ( +
+
+
+ + {SHOWCASE_EDGES.map(([from, to]) => { + const source = byId.get(from) + const target = byId.get(to) + if (!source || !target) return null + return ( + + ) + })} + + {SHOWCASE_BLOCKS.map((block) => ( +
+ +
+ ))} +
+
+
+ ) +} diff --git a/apps/sim/app/(landing)/components/features/components/build-callout/index.ts b/apps/sim/app/(landing)/components/features/components/build-callout/index.ts new file mode 100644 index 00000000000..44cc8ab0e45 --- /dev/null +++ b/apps/sim/app/(landing)/components/features/components/build-callout/index.ts @@ -0,0 +1 @@ +export { BuildCallout } from './build-callout' diff --git a/apps/sim/app/(landing)/components/features/components/callout-frame/callout-frame.tsx b/apps/sim/app/(landing)/components/features/components/callout-frame/callout-frame.tsx new file mode 100644 index 00000000000..4ff2f12956f --- /dev/null +++ b/apps/sim/app/(landing)/components/features/components/callout-frame/callout-frame.tsx @@ -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 ( +
+
+ {children} +
+
+ ) +} diff --git a/apps/sim/app/(landing)/components/features/components/callout-frame/index.ts b/apps/sim/app/(landing)/components/features/components/callout-frame/index.ts new file mode 100644 index 00000000000..a4d7e1dc37f --- /dev/null +++ b/apps/sim/app/(landing)/components/features/components/callout-frame/index.ts @@ -0,0 +1 @@ +export { CalloutFrame } from './callout-frame' diff --git a/apps/sim/app/(landing)/components/features/components/feature-card/feature-card.tsx b/apps/sim/app/(landing)/components/features/components/feature-card/feature-card.tsx new file mode 100644 index 00000000000..cea43fd45f3 --- /dev/null +++ b/apps/sim/app/(landing)/components/features/components/feature-card/feature-card.tsx @@ -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 (`

` - the section owns the single `

`). */ + 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: `

`, 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 `

` 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 ( +
+ + {eyebrow} + + + +
+ + {eyebrow} + +

+ {title} +

+

+ {description} +

+ {href && linkLabel && ( + + {linkLabel} + + + )} +
+
+ ) +} diff --git a/apps/sim/app/(landing)/components/features/components/feature-card/index.ts b/apps/sim/app/(landing)/components/features/components/feature-card/index.ts new file mode 100644 index 00000000000..fcb1594becb --- /dev/null +++ b/apps/sim/app/(landing)/components/features/components/feature-card/index.ts @@ -0,0 +1 @@ +export { FeatureCard } from './feature-card' diff --git a/apps/sim/app/(landing)/components/features/components/feature-stage/feature-stage.tsx b/apps/sim/app/(landing)/components/features/components/feature-stage/feature-stage.tsx deleted file mode 100644 index f8dc7497f10..00000000000 --- a/apps/sim/app/(landing)/components/features/components/feature-stage/feature-stage.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import type { ReactNode } from 'react' -import { cn } from '@sim/emcn' -import type { SidebarView } from '@/app/(landing)/components/landing-preview/components/landing-preview-sidebar/landing-preview-sidebar' -import { LandingPreviewMount } from '@/app/(landing)/components/landing-preview/landing-preview-mount' - -/** - * One capability beat in {@link Features}: a copy block over a static platform - * backdrop with an elevated, real-Sim-UI callout floating on the left - * (Linear's "callout over a faded platform" pattern). The backdrop is the live - * workspace rendered static (`autoplay={false}`) on whichever {@link SidebarView} - * matches the beat; three intersected mask layers dissolve its left edge (behind - * the callout), its far-right edge, and its bottom-right corner into the page so - * only the relevant surface stays crisp. - * - * At and below `lg` (where {@link Features} reflows into a compact 2x2 grid, then - * a single column on phones) the floating callout is hidden and the edge masks - * are dropped, so each cell shows the full un-masked preview beneath its copy. - * - * Heading is an `

` - the section owns the single `

`, each beat is an item - * beneath it. - */ -const BACKDROP_MASK = - '[-webkit-mask-composite:source-in] [-webkit-mask-image:linear-gradient(to_right,transparent,#000_48%),linear-gradient(to_left,transparent,#000_16%),linear-gradient(to_top,transparent,#000_30%),radial-gradient(70%_70%_at_100%_100%,transparent,#000_58%)] [mask-composite:intersect] [mask-image:linear-gradient(to_right,transparent,#000_48%),linear-gradient(to_left,transparent,#000_16%),linear-gradient(to_top,transparent,#000_30%),radial-gradient(70%_70%_at_100%_100%,transparent,#000_58%)]' - -interface FeatureStageProps { - /** Capability name shown as a quiet kicker (e.g. "Mothership", "Pod"). */ - eyebrow: string - /** The beat's headline. */ - title: string - /** Supporting line beneath the headline. */ - description: string - /** Staged platform view for the backdrop. Defaults to `'workflow'`. */ - view?: SidebarView - /** Workflow staged when `view` is `'workflow'`. */ - workflowId?: string - /** The elevated real-UI callout (a {@link CalloutFrame}-wrapped surface). */ - callout: ReactNode -} - -export function FeatureStage({ - eyebrow, - title, - description, - view, - workflowId, - callout, -}: FeatureStageProps) { - return ( -
-
- {eyebrow} -

- {title} -

-

- {description} -

-
- -
-
- -
-
{callout}
-
-
- ) -} - -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 white panel (`--border-1` - * hairline + soft shadow) that lifts a real Sim UI surface off the backdrop. - * 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 ( -
-
- {children} -
-
- ) -} diff --git a/apps/sim/app/(landing)/components/features/components/formation-graph/formation-graph.tsx b/apps/sim/app/(landing)/components/features/components/formation-graph/formation-graph.tsx deleted file mode 100644 index 6fb18a6b1ea..00000000000 --- a/apps/sim/app/(landing)/components/features/components/formation-graph/formation-graph.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { cn } from '@sim/emcn' -import { AgentIcon, AnthropicIcon } from '@/components/icons' - -/** - * FormationGraph - the Formation beat's callout: several agents running in - * parallel that fan *in* to one merged result. Deliberately the inverse shape of - * every other beat (linear trigger → agent → action), and of the workflow behind - * it - so it reads as "many agents on one problem," not another branching flow. - * - * Free-form (no white block): the agent cards sit directly on the faded platform - * backdrop, wired with `#c9c9c9` bezier edges, exactly as they read on the - * canvas. The three parallel agents carry a live "Running" status; they converge - * on a single Synthesis agent that merges their work. Decorative (`aria-hidden`). - */ -const CARD = - 'absolute w-[184px] rounded-[8px] border border-[var(--border-1)] bg-[var(--surface-2)]' -const TILE = - 'flex size-[24px] flex-shrink-0 items-center justify-center rounded-[6px] bg-[var(--text-body)]' - -interface AgentCardProps { - className: string - name: string - /** Live status shown on the parallel agents (omitted on the merge node). */ - status?: string - /** Model row shown on the merge node. */ - model?: string -} - -function AgentCard({ className, name, status, model }: AgentCardProps) { - return ( -
-
-
- -
- - {name} - - {status && ( - - - {status} - - )} -
- {model && ( -
- Model - - - claude-opus-4.8 - -
- )} -
- ) -} - -export function FormationGraph() { - return ( - - ) -} diff --git a/apps/sim/app/(landing)/components/features/components/integrations-callout/integrations-callout.tsx b/apps/sim/app/(landing)/components/features/components/integrations-callout/integrations-callout.tsx index d567e7ffe75..7974064feb3 100644 --- a/apps/sim/app/(landing)/components/features/components/integrations-callout/integrations-callout.tsx +++ b/apps/sim/app/(landing)/components/features/components/integrations-callout/integrations-callout.tsx @@ -1,62 +1,38 @@ -import type { ComponentType, SVGProps } from 'react' -import { Search } from '@sim/emcn' -import { - GithubIcon, - GmailIcon, - GoogleDriveIcon, - HubspotIcon, - JiraIcon, - LinearIcon, - NotionIcon, - PostgresIcon, - SalesforceIcon, - SlackIcon, -} from '@/components/icons' -import { CalloutFrame } from '@/app/(landing)/components/features/components/feature-stage/feature-stage' +import Image from 'next/image' +import { CalloutFrame } from '@/app/(landing)/components/features/components/callout-frame' /** - * The Integrate beat's callout - a static recreation of Sim's integration - * picker: a search over the 1,000+ connectors, each with its real brand mark - * and category, the way you'd wire a tool into an agent. The lower rows dissolve - * through the frame's foot fade, implying the rest of the catalog. Decorative. + * 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. */ -interface Integration { - name: string - Icon: ComponentType> - category: string -} - -const INTEGRATIONS: Integration[] = [ - { name: 'Slack', Icon: SlackIcon, category: 'Messaging' }, - { name: 'Gmail', Icon: GmailIcon, category: 'Email' }, - { name: 'HubSpot', Icon: HubspotIcon, category: 'CRM' }, - { name: 'Salesforce', Icon: SalesforceIcon, category: 'CRM' }, - { name: 'Notion', Icon: NotionIcon, category: 'Docs' }, - { name: 'Jira', Icon: JiraIcon, category: 'Issues' }, - { name: 'GitHub', Icon: GithubIcon, category: 'Code' }, - { name: 'Linear', Icon: LinearIcon, category: 'Issues' }, - { name: 'Google Drive', Icon: GoogleDriveIcon, category: 'Storage' }, - { name: 'Postgres', Icon: PostgresIcon, category: 'Database' }, -] - export function IntegrationsCallout() { return ( - -
-
- - Search 1,000+ integrations… -
-
- {INTEGRATIONS.map(({ name, Icon, category }) => ( -
- - {name} - {category} -
- ))} -
-
-
+
+ + + +
) } diff --git a/apps/sim/app/(landing)/components/features/components/knowledge-callout/knowledge-callout.tsx b/apps/sim/app/(landing)/components/features/components/knowledge-callout/knowledge-callout.tsx index bf1346cfdc1..2aec7a67c27 100644 --- a/apps/sim/app/(landing)/components/features/components/knowledge-callout/knowledge-callout.tsx +++ b/apps/sim/app/(landing)/components/features/components/knowledge-callout/knowledge-callout.tsx @@ -1,45 +1,35 @@ -import { Database, File } from '@sim/emcn' -import { CalloutFrame } from '@/app/(landing)/components/features/components/feature-stage/feature-stage' +import Image from 'next/image' +import { CalloutFrame } from '@/app/(landing)/components/features/components/callout-frame' /** - * The Ingest-context beat's callout - a static recreation of a Sim knowledge - * base: uploaded docs and synced sources, chunked and embedded so agents can - * read them semantically. The chunk counts stand in for the vector index; the - * lower rows dissolve through the frame's foot fade. Decorative. + * 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. */ -interface Source { - name: string - meta: string -} - -const SOURCES: Source[] = [ - { name: 'Product docs', meta: '128 chunks' }, - { name: 'Sales playbook.pdf', meta: '64 chunks' }, - { name: 'Support macros', meta: '212 chunks' }, - { name: 'Engineering · Notion', meta: 'Synced' }, - { name: 'Pricing & plans', meta: '18 chunks' }, - { name: 'Onboarding guide', meta: '47 chunks' }, -] - export function KnowledgeCallout() { return ( - -
-
- - Knowledge base - 6 sources · embedded -
-
- {SOURCES.map(({ name, meta }) => ( -
- - {name} - {meta} -
- ))} -
-
-
+
+ + + +
) } diff --git a/apps/sim/app/(landing)/components/features/components/logs-callout/index.ts b/apps/sim/app/(landing)/components/features/components/logs-callout/index.ts new file mode 100644 index 00000000000..572eadac8b3 --- /dev/null +++ b/apps/sim/app/(landing)/components/features/components/logs-callout/index.ts @@ -0,0 +1 @@ +export { LogsCallout } from './logs-callout' diff --git a/apps/sim/app/(landing)/components/features/components/logs-callout/logs-callout.tsx b/apps/sim/app/(landing)/components/features/components/logs-callout/logs-callout.tsx new file mode 100644 index 00000000000..911215df460 --- /dev/null +++ b/apps/sim/app/(landing)/components/features/components/logs-callout/logs-callout.tsx @@ -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 ( +
+ + + +
+ ) +} diff --git a/apps/sim/app/(landing)/components/features/components/logs-table-preview/index.ts b/apps/sim/app/(landing)/components/features/components/logs-table-preview/index.ts deleted file mode 100644 index 151a3fb9afd..00000000000 --- a/apps/sim/app/(landing)/components/features/components/logs-table-preview/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { LogsTablePreview } from './logs-table-preview' diff --git a/apps/sim/app/(landing)/components/features/components/logs-table-preview/logs-table-preview.tsx b/apps/sim/app/(landing)/components/features/components/logs-table-preview/logs-table-preview.tsx deleted file mode 100644 index 7407cbd9e5e..00000000000 --- a/apps/sim/app/(landing)/components/features/components/logs-table-preview/logs-table-preview.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import type { BadgeProps } from '@sim/emcn' -import { ArrowUpDown, Badge, Library, ListFilter, Workflow } from '@sim/emcn' - -/** - * LogsTablePreview - a static recreation of the Sim Logs page, used as the - * graphic inside the Logs card's {@link PlatformCorner} white block. It shows a - * fragment of the real Logs view: the header (`Logs` title + Filter/Sort), then - * the run table - workflow name, status, trigger, cost, and duration - with the - * platform's `Badge` chrome (dotted status pills, trigger pills). - * - * Purely presentational (no search/sort state) - it's a corner of the product - * framed as a screenshot, so the panel's right columns and lower rows dissolve - * through the {@link PlatformCorner} corner fade. - */ -type BadgeVariant = BadgeProps['variant'] - -interface LogRow { - workflow: string - date: string - statusLabel: string - statusVariant: BadgeVariant - trigger: string - cost: string - duration: string -} - -const ROWS: LogRow[] = [ - { - workflow: 'Customer Onboarding', - date: 'Apr 1 10:42 AM', - statusLabel: 'Running', - statusVariant: 'gray', - trigger: 'Webhook', - cost: '–', - duration: '–', - }, - { - workflow: 'Lead Enrichment', - date: 'Apr 1 09:15 AM', - statusLabel: 'Completed', - statusVariant: 'gray-secondary', - trigger: 'API', - cost: '2 credits', - duration: '1.2s', - }, - { - workflow: 'Email Campaign', - date: 'Apr 1 08:30 AM', - statusLabel: 'Completed', - statusVariant: 'gray-secondary', - trigger: 'Schedule', - cost: '2 credits', - duration: '0.8s', - }, - { - workflow: 'Data Pipeline', - date: 'Mar 31 10:14 PM', - statusLabel: 'Completed', - statusVariant: 'gray-secondary', - trigger: 'Webhook', - cost: '7 credits', - duration: '4.1s', - }, - { - workflow: 'Support Triage', - date: 'Mar 31 07:22 PM', - statusLabel: 'Error', - statusVariant: 'gray', - trigger: 'API', - cost: '1 credit', - duration: '2.7s', - }, - { - workflow: 'Invoice Processing', - date: 'Mar 31 06:45 PM', - statusLabel: 'Completed', - statusVariant: 'gray-secondary', - trigger: 'Manual', - cost: '2 credits', - duration: '0.9s', - }, - { - workflow: 'Content Moderator', - date: 'Mar 31 05:11 PM', - statusLabel: 'Completed', - statusVariant: 'gray-secondary', - trigger: 'Schedule', - cost: '3 credits', - duration: '1.6s', - }, -] - -const COL_HEADERS = ['Workflow', 'Status', 'Trigger', 'Cost', 'Duration'] as const - -export function LogsTablePreview() { - return ( -
-
-
- - Logs -
-
- - - Filter - - - - Sort - -
-
- - - - - - - - - - - - {COL_HEADERS.map((label) => ( - - ))} - - - - {ROWS.map((row) => ( - - - - - - - - ))} - -
- {label} -
-
- - - {row.workflow} - -
-
- - {row.statusLabel} - - - - {row.trigger} - - - {row.cost} - - {row.duration} -
-
- ) -} diff --git a/apps/sim/app/(landing)/components/features/features.tsx b/apps/sim/app/(landing)/components/features/features.tsx index 5175913d308..8c089c58377 100644 --- a/apps/sim/app/(landing)/components/features/features.tsx +++ b/apps/sim/app/(landing)/components/features/features.tsx @@ -1,102 +1,94 @@ -import { - CalloutFrame, - FeatureStage, -} from '@/app/(landing)/components/features/components/feature-stage/feature-stage' -import { FormationGraph } from '@/app/(landing)/components/features/components/formation-graph/formation-graph' +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 { LogsTablePreview } from '@/app/(landing)/components/features/components/logs-table-preview' - -/** Static callouts are hoisted so each keeps a stable element identity. */ -const INTEGRATIONS_CALLOUT = -const KNOWLEDGE_CALLOUT = -const FORMATION_CALLOUT = -const MONITOR_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 uses one shared {@link FeatureStage} template: a copy - * block over a static, edge-faded platform backdrop with an elevated real-UI - * callout floating on the left. + * (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 `

` is `sr-only` - each beat carries its own visible `

`, * 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 `
` flex `gap` in `landing.tsx`; - * this section carries no vertical padding. Horizontal padding (`px-12`) matches - * the navbar and hero; the section is capped and centered at the shared - * `max-w-[1446px]`. + * 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 four beats sit in a one-column grid that, like the rest of the page, - * scales down via `max-*` overrides. From `xl` up (and through the 1024-1279 - * band) they stay a single full-width column on the 120px inter-section rhythm, - * so a beat break reads identically to a section break. At and below `lg` - * (≤1023) they reflow into a compact 2x2 grid; on phones (≤639) they drop back to - * a single column. {@link FeatureStage} hides its floating callout and drops its - * edge masks at the same `lg` breakpoint, so each grid cell shows the un-masked - * preview under its copy. + * 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 ( -
+

Integrate your tools, give Sim context, build agents, and monitor every run.

-
+
{/* Integrate: bring your stack in. */} - + href='/integrations' + linkLabel='Explore integrations' + backdropSrc='/landing/feature-integrate-backdrop.jpg' + > + + {/* Context: store data semantically. */} - + backdropSrc='/landing/feature-context-backdrop.jpg' + mediaSide='right' + > + + {/* Build: wire agent logic in the visual builder. */} - + href='/workflows' + linkLabel='Explore the workflow builder' + > + + {/* Monitor: watch every run. */} - + backdropSrc='/landing/feature-monitor-backdrop.jpg' + mediaSide='right' + flushBottom + > + +
+ + {/* 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). */} +
) } diff --git a/apps/sim/app/(landing)/components/footer/footer.tsx b/apps/sim/app/(landing)/components/footer/footer.tsx index 6e51d218771..0607613b4e1 100644 --- a/apps/sim/app/(landing)/components/footer/footer.tsx +++ b/apps/sim/app/(landing)/components/footer/footer.tsx @@ -14,7 +14,7 @@ import { MODEL_PROVIDERS_WITH_CATALOGS } from '@/app/(landing)/models/utils' * hairline separates it from the page and spans the full viewport width * (edge-to-edge): the border lives on the full-width `