diff --git a/apps/sim/app/(landing)/components/cta/components/cta-chat.tsx b/apps/sim/app/(landing)/components/cta/components/cta-chat.tsx deleted file mode 100644 index 21f62a5a1ef..00000000000 --- a/apps/sim/app/(landing)/components/cta/components/cta-chat.tsx +++ /dev/null @@ -1,32 +0,0 @@ -'use client' - -import { useState } from 'react' -import { LandingPreviewChatInput } from '@/app/(landing)/components/landing-preview/components/landing-preview-chat/chat-input' -import { useLandingSubmit } from '@/app/(landing)/components/landing-preview/hooks/use-landing-submit' -import { useAnimatedPlaceholder } from '@/hooks/use-animated-placeholder' - -/** - * Pre-footer CTA chat input - the page's final conversion surface. A real, - * interactive copy of the Mothership chat input: the visitor types their first - * prompt, and {@link useLandingSubmit} stashes it in browser storage and routes - * to `/signup` so the message survives the auth hop and lands them in Sim. The - * placeholder cycles the same "Ask Sim to …" examples as the product's home - * empty state, so the CTA reads as the front door to the workspace. - */ -export function CtaChat() { - const [value, setValue] = useState('') - const placeholder = useAnimatedPlaceholder() - const submit = useLandingSubmit() - - return ( -
- submit(value)} - placeholder={placeholder} - shadow - /> -
- ) -} diff --git a/apps/sim/app/(landing)/components/hero/components/hero-visual/hero-visual.tsx b/apps/sim/app/(landing)/components/hero/components/hero-visual/hero-visual.tsx index 9fb215f17cc..fb3ccaff2ca 100644 --- a/apps/sim/app/(landing)/components/hero/components/hero-visual/hero-visual.tsx +++ b/apps/sim/app/(landing)/components/hero/components/hero-visual/hero-visual.tsx @@ -913,11 +913,16 @@ export function HeroVisual() { return () => cancelAnimationFrame(raf) }, [loaderPainting, paintFrame]) + const phaseRef = useRef(phase) useEffect(() => { - const onResize = () => positionCursor(phase, true) + phaseRef.current = phase + }, [phase]) + + useEffect(() => { + const onResize = () => positionCursor(phaseRef.current, true) window.addEventListener('resize', onResize) return () => window.removeEventListener('resize', onResize) - }, [phase, positionCursor]) + }, [positionCursor]) useEffect( () => () => { diff --git a/apps/sim/app/(landing)/components/landing-preview/landing-preview-mount.tsx b/apps/sim/app/(landing)/components/landing-preview/landing-preview-mount.tsx index 204d0cc50ad..58d82704c2a 100644 --- a/apps/sim/app/(landing)/components/landing-preview/landing-preview-mount.tsx +++ b/apps/sim/app/(landing)/components/landing-preview/landing-preview-mount.tsx @@ -1,18 +1,30 @@ 'use client' +import { useEffect, useRef, useState } from 'react' import dynamic from 'next/dynamic' import type { SidebarView } from '@/app/(landing)/components/landing-preview/components/landing-preview-sidebar/landing-preview-sidebar' +/** Dimension-stable placeholder sized to the preview's exact footprint (zero CLS). */ +const PLACEHOLDER_CLASS = 'aspect-[1116/615] w-full rounded bg-[var(--surface-1)]' + +/** + * Load the preview chunk a little before it scrolls into view so it's ready by + * the time the user reaches it, without paying for it on initial load. + */ +const PRELOAD_ROOT_MARGIN = '400px' + /** * Client mount for the {@link LandingPreview} - the heavy, animated workspace * island (framer-motion + reactflow). Isolated here so the sections that show it * stay Server Components: only this leaf is `'use client'`. * * Loaded with `ssr: false` so the framer-motion/reactflow bundle never ships in - * the server-rendered HTML, and behind a dimension-stable placeholder sized to - * the preview's exact `aspect-[1116/615]` footprint so there is zero layout - * shift while it streams in. The placeholder fills with the canvas surface - * (`--surface-1`) so there is no flash as the island mounts. + * the server-rendered HTML, and **gated on viewport proximity**: the chunk only + * downloads once an {@link IntersectionObserver} reports the mount is near the + * viewport, so the below-the-fold previews don't pull the heavy bundle into the + * initial homepage load. A dimension-stable placeholder (the preview's exact + * `aspect-[1116/615]` footprint, filled with the canvas surface) holds the space + * before and during load, so there is zero layout shift or flash. */ const LandingPreview = dynamic( () => @@ -21,7 +33,7 @@ const LandingPreview = dynamic( ), { ssr: false, - loading: () =>
, + loading: () =>
, } ) @@ -35,5 +47,36 @@ interface LandingPreviewMountProps { } export function LandingPreviewMount({ autoplay, view, workflowId }: LandingPreviewMountProps) { - return + const ref = useRef(null) + const [inView, setInView] = useState(false) + + useEffect(() => { + if (inView) return + // Graceful degradation: without IntersectionObserver support, load eagerly + // rather than leave the preview stuck on its placeholder. + if (typeof IntersectionObserver === 'undefined') { + setInView(true) + return + } + const el = ref.current + if (!el) return + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) setInView(true) + }, + { rootMargin: PRELOAD_ROOT_MARGIN } + ) + observer.observe(el) + return () => observer.disconnect() + }, [inView]) + + return ( +
+ {inView ? ( + + ) : ( +
+ )} +
+ ) } diff --git a/apps/sim/app/(landing)/integrations/components/integration-card.tsx b/apps/sim/app/(landing)/integrations/components/integration-card.tsx index 646e47417b3..3749dd00a94 100644 --- a/apps/sim/app/(landing)/integrations/components/integration-card.tsx +++ b/apps/sim/app/(landing)/integrations/components/integration-card.tsx @@ -1,4 +1,5 @@ import type { ComponentType, SVGProps } from 'react' +import { memo } from 'react' import Link from 'next/link' import type { Integration } from '@/lib/integrations' import { ChevronArrow } from '@/app/(landing)/components/chevron-arrow' @@ -46,7 +47,10 @@ export function IntegrationCard({ integration, IconComponent }: IntegrationItemP * Integration list row - matches blog remaining post pattern. * Each row followed by an h-px divider. */ -export function IntegrationRow({ integration, IconComponent }: IntegrationItemProps) { +export const IntegrationRow = memo(function IntegrationRow({ + integration, + IconComponent, +}: IntegrationItemProps) { const { slug, name, description, bgColor } = integration return ( @@ -80,4 +84,4 @@ export function IntegrationRow({ integration, IconComponent }: IntegrationItemPr
) -} +}) diff --git a/apps/sim/app/(landing)/integrations/components/integration-grid.tsx b/apps/sim/app/(landing)/integrations/components/integration-grid.tsx index a78827c500f..a226fec75cc 100644 --- a/apps/sim/app/(landing)/integrations/components/integration-grid.tsx +++ b/apps/sim/app/(landing)/integrations/components/integration-grid.tsx @@ -1,5 +1,6 @@ 'use client' +import { useMemo } from 'react' import { ChipInput, Search } from '@sim/emcn' import { debounce, useQueryStates } from 'nuqs' import { blockTypeToIconMap, formatIntegrationType, type Integration } from '@/lib/integrations' @@ -28,29 +29,42 @@ export function IntegrationGrid({ integrations }: IntegrationGridProps) { ) const activeCategory = category || null - const counts = new Map() - for (const i of integrations) { - if (i.integrationType) { - counts.set(i.integrationType, (counts.get(i.integrationType) || 0) + 1) + // Category facets and a per-integration lowercased search index, derived once + // from the (stable) integration list instead of rebuilt on every keystroke. + // The index keeps each searchable field as its own entry so matching stays + // identical to a per-field `includes` (no cross-field boundary matches). + const { availableCategories, searchIndex } = useMemo(() => { + const counts = new Map() + const searchIndex = new Map() + for (const i of integrations) { + if (i.integrationType) { + counts.set(i.integrationType, (counts.get(i.integrationType) || 0) + 1) + } + searchIndex.set(i.type, [ + i.name.toLowerCase(), + i.description.toLowerCase(), + ...i.operations.flatMap((op) => [op.name.toLowerCase(), op.description.toLowerCase()]), + ...i.triggers.map((t) => t.name.toLowerCase()), + ]) } - } - const availableCategories = Array.from(counts.entries()) - .sort((a, b) => b[1] - a[1]) - .map(([key]) => key) + return { + availableCategories: Array.from(counts.entries()) + .sort((a, b) => b[1] - a[1]) + .map(([key]) => key), + searchIndex, + } + }, [integrations]) const q = query.trim().toLowerCase() - const filtered = integrations.filter((i) => { - if (activeCategory && i.integrationType !== activeCategory) return false - if (!q) return true - return ( - i.name.toLowerCase().includes(q) || - i.description.toLowerCase().includes(q) || - i.operations.some( - (op) => op.name.toLowerCase().includes(q) || op.description.toLowerCase().includes(q) - ) || - i.triggers.some((t) => t.name.toLowerCase().includes(q)) - ) - }) + const filtered = useMemo( + () => + integrations.filter((i) => { + if (activeCategory && i.integrationType !== activeCategory) return false + if (!q) return true + return searchIndex.get(i.type)?.some((field) => field.includes(q)) ?? false + }), + [integrations, searchIndex, q, activeCategory] + ) return (
diff --git a/apps/sim/app/_styles/fonts/season/SeasonSans-Bold.woff b/apps/sim/app/_styles/fonts/season/SeasonSans-Bold.woff deleted file mode 100644 index 704a2e2f63f..00000000000 Binary files a/apps/sim/app/_styles/fonts/season/SeasonSans-Bold.woff and /dev/null differ diff --git a/apps/sim/app/_styles/fonts/season/SeasonSans-Bold.woff2 b/apps/sim/app/_styles/fonts/season/SeasonSans-Bold.woff2 deleted file mode 100644 index 4c66183c587..00000000000 Binary files a/apps/sim/app/_styles/fonts/season/SeasonSans-Bold.woff2 and /dev/null differ diff --git a/apps/sim/app/_styles/fonts/season/SeasonSans-Heavy.woff b/apps/sim/app/_styles/fonts/season/SeasonSans-Heavy.woff deleted file mode 100644 index 567c2d8350a..00000000000 Binary files a/apps/sim/app/_styles/fonts/season/SeasonSans-Heavy.woff and /dev/null differ diff --git a/apps/sim/app/_styles/fonts/season/SeasonSans-Heavy.woff2 b/apps/sim/app/_styles/fonts/season/SeasonSans-Heavy.woff2 deleted file mode 100644 index 99d28b0ed7c..00000000000 Binary files a/apps/sim/app/_styles/fonts/season/SeasonSans-Heavy.woff2 and /dev/null differ diff --git a/apps/sim/app/_styles/fonts/season/SeasonSans-Light.woff b/apps/sim/app/_styles/fonts/season/SeasonSans-Light.woff deleted file mode 100644 index 067216a9944..00000000000 Binary files a/apps/sim/app/_styles/fonts/season/SeasonSans-Light.woff and /dev/null differ diff --git a/apps/sim/app/_styles/fonts/season/SeasonSans-Light.woff2 b/apps/sim/app/_styles/fonts/season/SeasonSans-Light.woff2 deleted file mode 100644 index 8a09772bf27..00000000000 Binary files a/apps/sim/app/_styles/fonts/season/SeasonSans-Light.woff2 and /dev/null differ diff --git a/apps/sim/app/_styles/fonts/season/SeasonSans-Medium.woff b/apps/sim/app/_styles/fonts/season/SeasonSans-Medium.woff deleted file mode 100644 index e6ed31d2d50..00000000000 Binary files a/apps/sim/app/_styles/fonts/season/SeasonSans-Medium.woff and /dev/null differ diff --git a/apps/sim/app/_styles/fonts/season/SeasonSans-Medium.woff2 b/apps/sim/app/_styles/fonts/season/SeasonSans-Medium.woff2 deleted file mode 100644 index 7ce737db982..00000000000 Binary files a/apps/sim/app/_styles/fonts/season/SeasonSans-Medium.woff2 and /dev/null differ diff --git a/apps/sim/app/_styles/fonts/season/SeasonSans-Regular.woff b/apps/sim/app/_styles/fonts/season/SeasonSans-Regular.woff deleted file mode 100644 index db82e64468d..00000000000 Binary files a/apps/sim/app/_styles/fonts/season/SeasonSans-Regular.woff and /dev/null differ diff --git a/apps/sim/app/_styles/fonts/season/SeasonSans-Regular.woff2 b/apps/sim/app/_styles/fonts/season/SeasonSans-Regular.woff2 deleted file mode 100644 index 2b98eb29b2b..00000000000 Binary files a/apps/sim/app/_styles/fonts/season/SeasonSans-Regular.woff2 and /dev/null differ diff --git a/apps/sim/app/_styles/fonts/season/SeasonSans-SemiBold.woff b/apps/sim/app/_styles/fonts/season/SeasonSans-SemiBold.woff deleted file mode 100644 index a8c35b1f237..00000000000 Binary files a/apps/sim/app/_styles/fonts/season/SeasonSans-SemiBold.woff and /dev/null differ diff --git a/apps/sim/app/_styles/fonts/season/SeasonSans-SemiBold.woff2 b/apps/sim/app/_styles/fonts/season/SeasonSans-SemiBold.woff2 deleted file mode 100644 index 073a47af351..00000000000 Binary files a/apps/sim/app/_styles/fonts/season/SeasonSans-SemiBold.woff2 and /dev/null differ diff --git a/apps/sim/app/_styles/fonts/season/SeasonSansUprightsVF.woff b/apps/sim/app/_styles/fonts/season/SeasonSansUprightsVF.woff deleted file mode 100644 index bead4f5df89..00000000000 Binary files a/apps/sim/app/_styles/fonts/season/SeasonSansUprightsVF.woff and /dev/null differ diff --git a/apps/sim/public/landing/sim-mothership.webp b/apps/sim/public/landing/sim-mothership.webp deleted file mode 100644 index 140d5f2ef0c..00000000000 Binary files a/apps/sim/public/landing/sim-mothership.webp and /dev/null differ diff --git a/apps/sim/public/static/mothership.gif b/apps/sim/public/static/mothership.gif deleted file mode 100644 index 7d35f17c572..00000000000 Binary files a/apps/sim/public/static/mothership.gif and /dev/null differ