chore(cleanup): react-doctor dead code elimination, landing + docs overhaul, component modernization (#4544)

* fix(react-doctor): remove unused export types, useEffect clearTimeout missing, a11y fixes

* fix(react-doctor): strip unused export types from contracts, copilot, stores, and components

Remove export keyword from type/interface declarations confirmed to have zero importers
across lib/api/contracts/tools/aws/, lib/api/contracts/*.ts, lib/copilot/generated/,
stores/workflows/workflow/types.ts, ee/access-control, ee/data-retention, lib/logs/types.ts,
and app/workspace component files. TypeScript and API validation both pass clean.

Reduces unused-types count from 394 → 181 and fully eliminates the ✗ critical
dead-code categories (exports, types, files now show as ⚠ warnings not ✗ errors).

* docs improvements

* fix(react-doctor): delete 50 unused files (dead barrels, unreachable components, stale utilities)

Remove confirmed-unused barrel index.ts files across stores/, connectors/, executor/,
lib/, and app/workspace/ that had zero importers. Also delete unreachable components
(chat-history-skeleton, trace-spans, logs-list, template-profile, enterprise landing
sections), stale utilities (buffered-stream, blob-to-data-url, queued-workflow-execution,
compute-edit-sequence), and obsolete generated/contract files. TypeScript passes clean.

* remove dead code

* cleanup

* more

* fix(blog): restore DiffControlsDemo for v0-5 blog post

* fix: restore ContactButton for enterprise blog post, export WIKIPEDIA_PAGE_CONTENT_OUTPUT_PROPERTIES

* fix(react-doc): restore stripped exports and remove server-only dependency

* chore(deps): update lockfile after removing server-only

* added back some exports

* docs

* more

* fix type issues

* tc

* fix docs search route

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(tag-dropdown): add missing isEqual import from es-toolkit

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Waleed
2026-05-10 12:00:05 -07:00
committed by GitHub
co-authored by Cursor
parent aae93f8228
commit 3ed8615b5d
927 changed files with 8492 additions and 15749 deletions
+6 -3
View File
@@ -306,9 +306,12 @@ export async function generateMetadata(props: {
siteName: 'Sim Documentation',
type: 'article',
locale: OG_LOCALE_MAP[lang] ?? 'en_US',
alternateLocale: i18n.languages
.filter((l) => l !== lang)
.map((l) => OG_LOCALE_MAP[l] ?? 'en_US'),
alternateLocale: i18n.languages.reduce<string[]>((locales, l) => {
if (l !== lang) {
locales.push(OG_LOCALE_MAP[l] ?? 'en_US')
}
return locales
}, []),
images: [
{
url: ogImageUrl,
+2 -9
View File
@@ -11,6 +11,7 @@ import {
import { Navbar } from '@/components/navbar/navbar'
import { SimLogoFull } from '@/components/ui/sim-logo'
import { i18n } from '@/lib/i18n'
import { serializeJsonLd } from '@/lib/json-ld'
import { source } from '@/lib/source'
import { DOCS_BASE_URL } from '@/lib/urls'
import '../global.css'
@@ -78,14 +79,6 @@ export default async function Layout({ children, params }: LayoutProps) {
},
},
inLanguage: lang,
potentialAction: {
'@type': 'SearchAction',
target: {
'@type': 'EntryPoint',
urlTemplate: `${DOCS_BASE_URL}/api/search?q={search_term_string}`,
},
'query-input': 'required name=search_term_string',
},
}
return (
@@ -97,7 +90,7 @@ export default async function Layout({ children, params }: LayoutProps) {
<head>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
dangerouslySetInnerHTML={{ __html: serializeJsonLd(structuredData) }}
/>
</head>
<body className='flex min-h-screen flex-col font-sans'>
+1 -1
View File
@@ -9,7 +9,7 @@ export default function NotFound() {
return (
<DocsPage>
<div className='flex min-h-[70vh] flex-col items-center justify-center gap-4 text-center'>
<h1 className='bg-gradient-to-b from-[#47d991] to-[#33c482] bg-clip-text font-bold text-8xl text-transparent'>
<h1 className='bg-gradient-to-b from-[#47d991] to-[#33c482] bg-clip-text font-semibold text-8xl text-transparent'>
404
</h1>
<h2 className='font-semibold text-2xl text-foreground'>Page Not Found</h2>
+53 -43
View File
@@ -1,3 +1,4 @@
import type { CSSProperties } from 'react'
import { ImageResponse } from 'next/og'
import type { NextRequest } from 'next/server'
@@ -8,6 +9,34 @@ const TITLE_FONT_SIZE = {
medium: 56,
small: 48,
} as const
const FONT_CACHE_REVALIDATE_SECONDS = 60 * 60 * 24 * 30
const OG_CONTAINER_STYLE = {
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
padding: '56px 64px',
background: '#121212',
fontFamily: 'Geist',
} satisfies CSSProperties
const OG_TITLE_STYLE = {
fontWeight: 500,
color: '#fafafa',
lineHeight: 1.2,
letterSpacing: '-0.02em',
} satisfies CSSProperties
const OG_FOOTER_STYLE = {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
} satisfies CSSProperties
const OG_DOMAIN_STYLE = {
fontSize: 20,
fontWeight: 400,
color: '#71717a',
} satisfies CSSProperties
function getTitleFontSize(title: string): number {
if (title.length > 45) return TITLE_FONT_SIZE.small
@@ -15,17 +44,34 @@ function getTitleFontSize(title: string): number {
return TITLE_FONT_SIZE.large
}
function getTitleStyle(title: string): CSSProperties {
return {
...OG_TITLE_STYLE,
fontSize: getTitleFontSize(title),
}
}
/**
* Loads a Google Font dynamically by fetching the CSS and extracting the font URL.
*/
async function loadGoogleFont(font: string, weights: string, text: string): Promise<ArrayBuffer> {
const url = `https://fonts.googleapis.com/css2?family=${font}:wght@${weights}&text=${encodeURIComponent(text)}`
const css = await (await fetch(url)).text()
const cssResponse = await fetch(url, {
next: { revalidate: FONT_CACHE_REVALIDATE_SECONDS },
})
if (!cssResponse.ok) {
throw new Error(`Failed to load font CSS: ${cssResponse.status} ${cssResponse.statusText}`)
}
const css = await cssResponse.text()
const resource = css.match(/src: url\((.+)\) format\('(opentype|truetype)'\)/)
if (resource) {
const response = await fetch(resource[1])
if (response.status === 200) {
const response = await fetch(resource[1], {
next: { revalidate: FONT_CACHE_REVALIDATE_SECONDS },
})
if (response.ok) {
return await response.arrayBuffer()
}
}
@@ -72,50 +118,14 @@ export async function GET(request: NextRequest) {
const fontData = await loadGoogleFont('Geist', '400;500;600', allText)
return new ImageResponse(
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
padding: '56px 64px',
background: '#121212', // Dark mode background matching docs (hsla 0, 0%, 7%)
fontFamily: 'Geist',
}}
>
<div style={OG_CONTAINER_STYLE}>
{/* Title at top */}
<span
style={{
fontSize: getTitleFontSize(title),
fontWeight: 500,
color: '#fafafa', // Light text matching docs
lineHeight: 1.2,
letterSpacing: '-0.02em',
}}
>
{title}
</span>
<span style={getTitleStyle(title)}>{title}</span>
{/* Footer: icon left, domain right */}
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
}}
>
<div style={OG_FOOTER_STYLE}>
<SimLogoFull />
<span
style={{
fontSize: 20,
fontWeight: 400,
color: '#71717a',
}}
>
docs.sim.ai
</span>
<span style={OG_DOMAIN_STYLE}>docs.sim.ai</span>
</div>
</div>,
{
+60 -32
View File
@@ -6,6 +6,28 @@ import { generateSearchEmbedding } from '@/lib/embeddings'
export const runtime = 'nodejs'
export const revalidate = 0
const DEFAULT_SEARCH_LIMIT = 10
const MAX_SEARCH_LIMIT = 20
function getSearchLimit(value: unknown): number {
const limit = Number.parseInt(String(value ?? DEFAULT_SEARCH_LIMIT), 10)
if (!Number.isFinite(limit) || limit <= 0) {
return DEFAULT_SEARCH_LIMIT
}
return Math.min(limit, MAX_SEARCH_LIMIT)
}
function getSearchParams(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
return {
query: searchParams.get('query') || searchParams.get('q') || '',
locale: searchParams.get('locale') || 'en',
limit: getSearchLimit(searchParams.get('limit')),
}
}
/**
* Hybrid search API endpoint
* - English: Vector embeddings + keyword search
@@ -13,10 +35,7 @@ export const revalidate = 0
*/
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const query = searchParams.get('query') || searchParams.get('q') || ''
const locale = searchParams.get('locale') || 'en'
const limit = Number.parseInt(searchParams.get('limit') || '10', 10)
const { query, locale, limit } = getSearchParams(request)
if (!query || query.trim().length === 0) {
return NextResponse.json([])
@@ -94,6 +113,10 @@ export async function GET(request: NextRequest) {
const keywordRankMap = new Map<string, number>()
keywordResults.forEach((r, idx) => keywordRankMap.set(r.chunkId, idx + 1))
const resultByChunkId = new Map<string, (typeof vectorResults)[number]>()
keywordResults.forEach((result) => resultByChunkId.set(result.chunkId, result))
vectorResults.forEach((result) => resultByChunkId.set(result.chunkId, result))
const allChunkIds = new Set([
...vectorResults.map((r) => r.chunkId),
...keywordResults.map((r) => r.chunkId),
@@ -109,9 +132,7 @@ export async function GET(request: NextRequest) {
const rrfScore = 1 / (k + vectorRank) + 1 / (k + keywordRank)
const result =
vectorResults.find((r) => r.chunkId === chunkId) ||
keywordResults.find((r) => r.chunkId === chunkId)
const result = resultByChunkId.get(chunkId)
if (result) {
scoredResults.push({ ...result, rrfScore })
@@ -167,31 +188,38 @@ export async function GET(request: NextRequest) {
const pathParts = result.sourceDocument
.replace('.mdx', '')
.split('/')
.filter((part) => part !== 'index' && !knownLocales.includes(part))
.map((part) => {
return part
.replace(/_/g, ' ')
.split(' ')
.map((word) => {
const acronyms = [
'api',
'mcp',
'sdk',
'url',
'http',
'json',
'xml',
'html',
'css',
'ai',
]
if (acronyms.includes(word.toLowerCase())) {
return word.toUpperCase()
}
return word.charAt(0).toUpperCase() + word.slice(1)
})
.join(' ')
})
.reduce<string[]>((parts, part) => {
if (part === 'index' || knownLocales.includes(part)) {
return parts
}
parts.push(
part
.replace(/_/g, ' ')
.split(' ')
.map((word) => {
const acronyms = [
'api',
'mcp',
'sdk',
'url',
'http',
'json',
'xml',
'html',
'css',
'ai',
]
if (acronyms.includes(word.toLowerCase())) {
return word.toUpperCase()
}
return word.charAt(0).toUpperCase() + word.slice(1)
})
.join(' ')
)
return parts
}, [])
return {
id: result.chunkId,
-1
View File
@@ -13,7 +13,6 @@ Disallow: /api/internal/
Disallow: /_next/static/
Disallow: /admin/
Allow: /
Allow: /api/search
Allow: /llms.txt
Allow: /llms-full.txt
Allow: /llms.mdx/
@@ -46,7 +46,7 @@ export function PageFooter({ previous, next }: PageFooterProps) {
Previous
</span>
<span className='flex items-center gap-1.5 font-[470] text-[rgba(0,0,0,0.7)] text-sm transition-colors group-hover:text-[rgba(0,0,0,0.88)] dark:text-[rgba(255,255,255,0.7)] dark:group-hover:text-[rgba(255,255,255,0.92)]'>
<ChevronLeft className='h-3.5 w-3.5 shrink-0' />
<ChevronLeft className='size-3.5 shrink-0' />
{previous.name}
</span>
</Link>
@@ -68,7 +68,7 @@ export function PageFooter({ previous, next }: PageFooterProps) {
</span>
<span className='flex items-center gap-1.5 font-[470] text-[rgba(0,0,0,0.7)] text-sm transition-colors group-hover:text-[rgba(0,0,0,0.88)] dark:text-[rgba(255,255,255,0.7)] dark:group-hover:text-[rgba(255,255,255,0.92)]'>
{next.name}
<ChevronRight className='h-3.5 w-3.5 shrink-0' />
<ChevronRight className='size-3.5 shrink-0' />
</span>
</Link>
) : (
@@ -90,7 +90,7 @@ export function PageFooter({ previous, next }: PageFooterProps) {
>
<svg
viewBox='0 0 24 24'
className='h-5 w-5 fill-gray-400 transition-colors hover:fill-gray-500 dark:fill-gray-500 dark:hover:fill-gray-400'
className='size-5 fill-neutral-400 transition-colors hover:fill-neutral-500 dark:fill-neutral-500 dark:hover:fill-neutral-400'
>
<path d={link.icon} />
</svg>
@@ -24,7 +24,7 @@ export function PageNavigationArrows({ previous, next }: PageNavigationArrowsPro
aria-label='Previous page'
title='Previous page'
>
<ChevronLeft className='h-4 w-4' />
<ChevronLeft className='size-4' />
</Link>
)}
{next && (
@@ -34,7 +34,7 @@ export function PageNavigationArrows({ previous, next }: PageNavigationArrowsPro
aria-label='Next page'
title='Next page'
>
<ChevronRight className='h-4 w-4' />
<ChevronRight className='size-4' />
</Link>
)}
</div>
@@ -1,6 +1,6 @@
'use client'
import { type ReactNode, useEffect, useState } from 'react'
import { type ReactNode, useState } from 'react'
import type { Folder, Item, Separator } from 'fumadocs-core/page-tree'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
@@ -103,12 +103,10 @@ export function SidebarFolder({ item, children }: { item: Folder; children: Reac
const isApiRef = isApiReferenceFolder(item)
const isOnApiRefPage = stripLangPrefix(pathname).startsWith('/api-reference')
const hasChildren = item.children.length > 0
const [open, setOpen] = useState(hasActiveChild || (isApiRef && isOnApiRefPage))
useEffect(() => {
setOpen(hasActiveChild || (isApiRef && isOnApiRefPage))
}, [hasActiveChild, isApiRef, isOnApiRefPage])
const defaultOpen = hasActiveChild || (isApiRef && isOnApiRefPage)
const [manualOpen, setManualOpen] = useState<{ pathname: string; open: boolean } | null>(null)
const open = manualOpen?.pathname === pathname ? manualOpen.open : defaultOpen
const toggleOpen = () => setManualOpen({ pathname, open: !open })
const active = item.index ? isActive(item.index.url, pathname, false) : false
if (item.index && !hasChildren) {
@@ -152,7 +150,7 @@ export function SidebarFolder({ item, children }: { item: Folder; children: Reac
</Link>
{hasChildren && (
<button
onClick={() => setOpen(!open)}
onClick={toggleOpen}
className={cn(
'rounded p-1 hover:bg-fd-accent/50',
'lg:cursor-pointer lg:rounded lg:p-1 lg:transition-colors lg:hover:bg-[#f2f2f2] lg:dark:hover:bg-[#262626]'
@@ -165,7 +163,7 @@ export function SidebarFolder({ item, children }: { item: Folder; children: Reac
</>
) : (
<button
onClick={() => setOpen(!open)}
onClick={toggleOpen}
className={cn(
'flex flex-1 items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors',
'text-fd-muted-foreground hover:bg-fd-accent/50',
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -14,12 +14,12 @@ export function LLMCopyButton({ content }: { content: string }) {
>
{checked ? (
<>
<Check className='h-3.5 w-3.5' />
<Check className='size-3.5' />
<span>Copied</span>
</>
) : (
<>
<Copy className='h-3.5 w-3.5' />
<Copy className='size-3.5' />
<span>Copy page</span>
</>
)}
+4 -3
View File
@@ -1,3 +1,4 @@
import { serializeJsonLd } from '@/lib/json-ld'
import { DOCS_BASE_URL } from '@/lib/urls'
interface StructuredDataProps {
@@ -103,14 +104,14 @@ export function StructuredData({
<script
type='application/ld+json'
dangerouslySetInnerHTML={{
__html: JSON.stringify(articleStructuredData),
__html: serializeJsonLd(articleStructuredData),
}}
/>
{breadcrumbStructuredData && (
<script
type='application/ld+json'
dangerouslySetInnerHTML={{
__html: JSON.stringify(breadcrumbStructuredData),
__html: serializeJsonLd(breadcrumbStructuredData),
}}
/>
)}
@@ -118,7 +119,7 @@ export function StructuredData({
<script
type='application/ld+json'
dangerouslySetInnerHTML={{
__html: JSON.stringify(softwareStructuredData),
__html: serializeJsonLd(softwareStructuredData),
}}
/>
)}
+54 -32
View File
@@ -19,23 +19,33 @@ interface ActionVideoProps {
export function ActionImage({ src, alt, enableLightbox = true }: ActionImageProps) {
const [isLightboxOpen, setIsLightboxOpen] = useState(false)
const handleClick = () => {
if (enableLightbox) {
setIsLightboxOpen(true)
}
}
const openLightbox = () => setIsLightboxOpen(true)
const image = (
<img
src={src}
alt={alt}
className={cn(
'inline-block w-full max-w-[200px] rounded border border-neutral-200 dark:border-neutral-700',
enableLightbox && 'transition-opacity group-hover:opacity-90'
)}
/>
)
return (
<>
<img
src={src}
alt={alt}
onClick={handleClick}
className={cn(
'inline-block w-full max-w-[200px] rounded border border-neutral-200 dark:border-neutral-700',
enableLightbox && 'cursor-pointer transition-opacity hover:opacity-90'
)}
/>
{enableLightbox ? (
<button
type='button'
onClick={openLightbox}
aria-label={`Open ${alt} in media viewer`}
className='group inline-block cursor-pointer rounded p-0 text-left'
>
{image}
</button>
) : (
image
)}
{enableLightbox && (
<Lightbox
isOpen={isLightboxOpen}
@@ -55,28 +65,40 @@ export function ActionVideo({ src, alt, enableLightbox = true }: ActionVideoProp
const [isLightboxOpen, setIsLightboxOpen] = useState(false)
const resolvedSrc = getAssetUrl(src)
const handleClick = () => {
if (enableLightbox) {
startTimeRef.current = videoRef.current?.currentTime ?? 0
setIsLightboxOpen(true)
}
const openLightbox = () => {
startTimeRef.current = videoRef.current?.currentTime ?? 0
setIsLightboxOpen(true)
}
const video = (
<video
ref={videoRef}
src={resolvedSrc}
autoPlay
loop
muted
playsInline
className={cn(
'inline-block w-full max-w-[200px] rounded border border-neutral-200 dark:border-neutral-700',
enableLightbox && 'transition-opacity group-hover:opacity-90'
)}
/>
)
return (
<>
<video
ref={videoRef}
src={resolvedSrc}
autoPlay
loop
muted
playsInline
onClick={handleClick}
className={cn(
'inline-block w-full max-w-[200px] rounded border border-neutral-200 dark:border-neutral-700',
enableLightbox && 'cursor-pointer transition-opacity hover:opacity-90'
)}
/>
{enableLightbox ? (
<button
type='button'
onClick={openLightbox}
aria-label={`Open ${alt} in media viewer`}
className='group inline-block cursor-pointer rounded p-0 text-left'
>
{video}
</button>
) : (
video
)}
{enableLightbox && (
<Lightbox
isOpen={isLightboxOpen}
+1 -1
View File
@@ -22,7 +22,7 @@ export function BlockInfoCard({
style={{ background: color }}
>
{ResolvedIcon ? (
<ResolvedIcon className='h-10 w-10 text-white' />
<ResolvedIcon className='size-10 text-white' />
) : (
<div className='font-mono text-white text-xl opacity-70'>{type.substring(0, 2)}</div>
)}
-27
View File
@@ -1,27 +0,0 @@
import { cva, type VariantProps } from 'class-variance-authority'
const variants = {
primary: 'bg-fd-primary text-fd-primary-foreground hover:bg-fd-primary/80',
outline: 'border hover:bg-fd-accent hover:text-fd-accent-foreground',
ghost: 'hover:bg-fd-accent hover:text-fd-accent-foreground',
secondary:
'border bg-fd-secondary text-fd-secondary-foreground hover:bg-fd-accent hover:text-fd-accent-foreground',
} as const
export const buttonVariants = cva(
'inline-flex items-center justify-center rounded-[5px] p-2 text-sm font-medium transition-colors duration-100 disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring',
{
variants: {
variant: variants,
color: variants,
size: {
sm: 'gap-1 px-2 py-1.5 text-xs',
icon: 'p-1.5 [&_svg]:size-5',
'icon-sm': 'p-1.5 [&_svg]:size-4.5',
'icon-xs': 'p-1 [&_svg]:size-4',
},
},
}
)
export type ButtonProps = VariantProps<typeof buttonVariants>
+34 -59
View File
@@ -1,8 +1,7 @@
'use client'
import * as React from 'react'
import type { ComponentPropsWithRef } from 'react'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import { Check } from 'lucide-react'
import { cn } from '@/lib/utils'
const ANIMATION_CLASSES =
@@ -11,67 +10,43 @@ const ANIMATION_CLASSES =
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 6, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
function DropdownMenuContent({
className,
sideOffset = 6,
ref,
...props
}: ComponentPropsWithRef<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
ANIMATION_CLASSES,
'z-50 max-h-[240px] min-w-[8rem] max-w-[220px] origin-[--radix-dropdown-menu-content-transform-origin] overflow-y-auto overflow-x-hidden rounded-lg border border-neutral-200 bg-white p-1.5 shadow-sm dark:border-neutral-800 dark:bg-neutral-900',
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuItem({
className,
ref,
...props
}: ComponentPropsWithRef<typeof DropdownMenuPrimitive.Item>) {
return (
<DropdownMenuPrimitive.Item
ref={ref}
sideOffset={sideOffset}
className={cn(
ANIMATION_CLASSES,
'z-50 max-h-[240px] min-w-[8rem] max-w-[220px] origin-[--radix-dropdown-menu-content-transform-origin] overflow-y-auto overflow-x-hidden rounded-lg border border-neutral-200 bg-white p-1.5 shadow-sm dark:border-neutral-800 dark:bg-neutral-900',
'relative flex min-w-0 cursor-pointer select-none items-center gap-2 rounded-[5px] px-2 py-1.5 font-medium text-[13px] text-neutral-700 outline-none transition-colors focus:bg-neutral-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:text-neutral-300 dark:focus:bg-neutral-800',
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
'relative flex min-w-0 cursor-pointer select-none items-center gap-2 rounded-[5px] px-2 py-1.5 font-medium text-[13px] text-neutral-700 outline-none transition-colors focus:bg-neutral-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:text-neutral-300 dark:focus:bg-neutral-800',
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-[5px] py-1.5 pr-2 pl-7 font-medium text-[13px] text-neutral-700 outline-none transition-colors focus:bg-neutral-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:text-neutral-300 dark:focus:bg-neutral-800',
className
)}
checked={checked}
{...props}
>
<span className='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'>
<DropdownMenuPrimitive.ItemIndicator>
<Check className='h-3.5 w-3.5' />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
)
}
export { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem }
+3 -2
View File
@@ -2,6 +2,7 @@
import { useState } from 'react'
import { ChevronRight } from 'lucide-react'
import { serializeJsonLd } from '@/lib/json-ld'
import { cn } from '@/lib/utils'
interface FAQItem {
@@ -76,13 +77,13 @@ export function FAQ({ items, title = 'Common Questions' }: FAQProps) {
<div className='mt-12'>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqSchema) }}
dangerouslySetInnerHTML={{ __html: serializeJsonLd(faqSchema) }}
/>
<h2 className='mb-4 font-[500] text-xl'>{title}</h2>
<div className='border-[rgba(0,0,0,0.08)] border-t border-b dark:border-[rgba(255,255,255,0.08)]'>
{items.map((item, index) => (
<div
key={index}
key={item.question}
className={cn(
index !== items.length - 1 &&
'border-[rgba(0,0,0,0.08)] border-b dark:border-[rgba(255,255,255,0.08)]'
+2 -2
View File
@@ -18,7 +18,7 @@ export function Heading({ as, className, ...props }: HeadingProps) {
return <As className={className} {...props} />
}
const handleClick = async (e: React.MouseEvent) => {
const copyHeadingLink = async (e: React.MouseEvent) => {
e.preventDefault()
const url = `${window.location.origin}${window.location.pathname}#${props.id}`
@@ -39,7 +39,7 @@ export function Heading({ as, className, ...props }: HeadingProps) {
return (
<As className={cn('group flex scroll-m-28 flex-row items-center gap-2', className)} {...props}>
<a href={`#${props.id}`} className='peer' onClick={handleClick}>
<a href={`#${props.id}`} className='peer' onClick={copyHeadingLink}>
{props.children}
</a>
{copied ? (
+26 -16
View File
@@ -19,25 +19,35 @@ export function Image({
}: ImageProps) {
const [isLightboxOpen, setIsLightboxOpen] = useState(false)
const handleImageClick = () => {
if (enableLightbox) {
setIsLightboxOpen(true)
}
}
const openLightbox = () => setIsLightboxOpen(true)
const image = (
<NextImage
className={cn(
'overflow-hidden rounded-xl border border-border object-cover',
enableLightbox && 'transition-opacity group-hover:opacity-95',
className
)}
alt={alt}
src={src}
{...props}
/>
)
return (
<>
<NextImage
className={cn(
'overflow-hidden rounded-xl border border-border object-cover',
enableLightbox && 'cursor-pointer transition-opacity hover:opacity-95',
className
)}
alt={alt}
src={src}
onClick={handleImageClick}
{...props}
/>
{enableLightbox ? (
<button
type='button'
onClick={openLightbox}
aria-label={`Open ${alt} in media viewer`}
className='group block w-full cursor-pointer rounded-xl p-0 text-left'
>
{image}
</button>
) : (
image
)}
{enableLightbox && (
<Lightbox
@@ -22,7 +22,7 @@ const languages = {
export function LanguageDropdown() {
const pathname = usePathname()
const params = useParams()
const router = useRouter()
const { push } = useRouter()
const langFromParams = params?.lang as string
const currentLang =
@@ -44,7 +44,7 @@ export function LanguageDropdown() {
newPath = `/${locale}${segments.length > 0 ? `/${segments.join('/')}` : '/introduction'}`
}
router.push(newPath)
push(newPath)
}
const languageEntries = Object.entries(languages)
@@ -80,7 +80,7 @@ export function LanguageDropdown() {
>
<span className='text-[13px]'>{lang.flag}</span>
<span className='flex-1'>{lang.name}</span>
{isSelected && <Check className='ml-auto h-3.5 w-3.5' />}
{isSelected && <Check className='ml-auto size-3.5' />}
</DropdownMenuItem>
)
})}
+41 -30
View File
@@ -1,6 +1,6 @@
'use client'
import { useEffect, useLayoutEffect, useRef } from 'react'
import { useEffect, useEffectEvent, useLayoutEffect, useRef } from 'react'
import { getAssetUrl } from '@/lib/utils'
interface LightboxProps {
@@ -14,33 +14,38 @@ interface LightboxProps {
export function Lightbox({ isOpen, onClose, src, alt, type, startTime }: LightboxProps) {
const overlayRef = useRef<HTMLDivElement>(null)
const mediaButtonRef = useRef<HTMLButtonElement>(null)
const videoRef = useRef<HTMLVideoElement>(null)
const closeLightbox = useEffectEvent(onClose)
useEffect(() => {
if (!isOpen) return
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose()
closeLightbox()
}
}
const handleClickOutside = (event: MouseEvent) => {
if (overlayRef.current && event.target === overlayRef.current) {
onClose()
closeLightbox()
}
}
if (isOpen) {
document.addEventListener('keydown', handleKeyDown)
document.addEventListener('click', handleClickOutside)
document.body.style.overflow = 'hidden'
}
const previousOverflow = document.body.style.overflow
document.addEventListener('keydown', handleKeyDown)
document.addEventListener('click', handleClickOutside)
document.body.style.overflow = 'hidden'
mediaButtonRef.current?.focus()
return () => {
document.removeEventListener('keydown', handleKeyDown)
document.removeEventListener('click', handleClickOutside)
document.body.style.overflow = 'unset'
document.body.style.overflow = previousOverflow
}
}, [isOpen, onClose])
}, [isOpen])
useLayoutEffect(() => {
if (isOpen && type === 'video' && videoRef.current && startTime != null && startTime > 0) {
@@ -59,26 +64,32 @@ export function Lightbox({ isOpen, onClose, src, alt, type, startTime }: Lightbo
aria-label='Media viewer'
>
<div className='relative max-h-full max-w-full overflow-hidden rounded-xl'>
{type === 'image' ? (
<img
src={src}
alt={alt}
className='max-h-[75vh] max-w-[75vw] cursor-pointer rounded-xl object-contain'
loading='lazy'
onClick={onClose}
/>
) : (
<video
ref={videoRef}
src={getAssetUrl(src)}
autoPlay
loop
muted
playsInline
className='max-h-[75vh] max-w-[75vw] cursor-pointer rounded-xl outline-none focus:outline-none'
onClick={onClose}
/>
)}
<button
ref={mediaButtonRef}
type='button'
onClick={onClose}
aria-label='Close media viewer'
className='block cursor-pointer rounded-xl p-0 outline-none focus-visible:ring-2 focus-visible:ring-white/70'
>
{type === 'image' ? (
<img
src={src}
alt={alt}
className='max-h-[75vh] max-w-[75vw] rounded-xl object-contain'
loading='lazy'
/>
) : (
<video
ref={videoRef}
src={getAssetUrl(src)}
autoPlay
loop
muted
playsInline
className='max-h-[75vh] max-w-[75vw] rounded-xl outline-none focus:outline-none'
/>
)}
</button>
</div>
</div>
)
+4 -4
View File
@@ -3,7 +3,7 @@
import { Search } from 'lucide-react'
export function SearchTrigger() {
const handleClick = () => {
const openSearchDialog = () => {
const event = new KeyboardEvent('keydown', {
key: 'k',
metaKey: true,
@@ -17,10 +17,10 @@ export function SearchTrigger() {
type='button'
data-search-trigger
className='flex h-8 w-[360px] cursor-pointer items-center gap-2 rounded-lg border border-border/50 bg-fd-muted/50 px-3 text-[13px] text-fd-muted-foreground transition-colors hover:bg-fd-muted'
onClick={handleClick}
onClick={openSearchDialog}
>
<Search className='h-3.5 w-3.5' />
<span>Search...</span>
<Search className='size-3.5' />
<span>Search&hellip;</span>
<kbd className='ml-auto flex items-center font-medium'>
<span className='text-[15px]'></span>
<span className='text-[12px]'>K</span>
+4 -52
View File
@@ -6,54 +6,6 @@ interface SimLogoProps {
className?: string
}
/**
* Sim logo with icon and text.
* The icon stays green (#33C482), text adapts to light/dark mode.
*/
export function SimLogo({ className }: SimLogoProps) {
return (
<svg
viewBox='720 440 320 320'
fill='none'
xmlns='http://www.w3.org/2000/svg'
className={cn('h-7 w-auto', className)}
aria-label='Sim'
>
{/* Green icon - top left shape with cutout */}
<path
fillRule='evenodd'
clipRule='evenodd'
d='M875.791 577.171C875.791 581.922 873.911 586.483 870.576 589.842L870.098 590.323C866.764 593.692 862.234 595.575 857.517 595.575H750.806C740.978 595.575 733 603.6 733 613.498V728.902C733 738.799 740.978 746.826 750.806 746.826H865.382C875.209 746.826 883.177 738.799 883.177 728.902V620.853C883.177 616.448 884.912 612.222 888.008 609.104C891.093 605.997 895.29 604.249 899.664 604.249H1008.16C1017.99 604.249 1025.96 596.224 1025.96 586.327V470.923C1025.96 461.025 1017.99 453 1008.16 453H893.586C883.759 453 875.791 461.025 875.791 470.923V577.171ZM910.562 477.566H991.178C996.922 477.566 1001.57 482.254 1001.57 488.029V569.22C1001.57 574.995 996.922 579.683 991.178 579.683H910.562C904.828 579.683 900.173 574.995 900.173 569.22V488.029C900.173 482.254 904.828 477.566 910.562 477.566Z'
fill='#33C482'
/>
{/* Green icon - bottom right square */}
<path
d='M1008.3 624.59H923.113C912.786 624.59 904.414 633.022 904.414 643.423V728.171C904.414 738.572 912.786 747.004 923.113 747.004H1008.3C1018.63 747.004 1027 738.572 1027 728.171V643.423C1027 633.022 1018.63 624.59 1008.3 624.59Z'
fill='#33C482'
/>
{/* Gradient overlay on bottom right square */}
<path
d='M1008.3 624.199H923.113C912.786 624.199 904.414 632.631 904.414 643.033V727.78C904.414 738.181 912.786 746.612 923.113 746.612H1008.3C1018.63 746.612 1027 738.181 1027 727.78V643.033C1027 632.631 1018.63 624.199 1008.3 624.199Z'
fill='url(#sim-logo-gradient)'
fillOpacity='0.2'
/>
<defs>
<linearGradient
id='sim-logo-gradient'
x1='904.414'
y1='624.199'
x2='978.836'
y2='698.447'
gradientUnits='userSpaceOnUse'
>
<stop />
<stop offset='1' stopOpacity='0' />
</linearGradient>
</defs>
</svg>
)
}
/**
* Full Sim logo with icon and "Sim" text.
* Uses the same SVG source as the landing page navbar for exact visual alignment.
@@ -85,7 +37,7 @@ export function SimLogoFull({ className }: SimLogoProps) {
<g transform='scale(.07483)'>
<path
clipRule='evenodd'
d='m142.793 124.175c0 4.75-1.88 9.312-5.216 12.671l-.478.481c-3.334 3.369-7.863 5.252-12.58 5.252h-106.7127c-9.82776 0-17.8063 8.026-17.8063 17.924v115.407c0 9.898 7.97854 17.924 17.8063 17.924h114.5767c9.828 0 17.796-8.026 17.796-17.924v-108.052c0-4.405 1.735-8.632 4.83-11.749 3.086-3.108 7.283-4.856 11.657-4.856h108.5c9.828 0 17.796-8.024 17.796-17.923v-115.4069c0-9.89798-7.968-17.9231-17.796-17.9231h-114.578c-9.827 0-17.795 8.02512-17.795 17.9231zm34.771-99.6079h80.617c5.744 0 10.389 4.6874 10.389 10.463v81.1939c0 5.774-4.645 10.463-10.389 10.463h-80.617c-5.734 0-10.389-4.689-10.389-10.463v-81.1939c0-5.7756 4.655-10.463 10.389-10.463z'
d='m142.79 124.17c0 4.75-1.88 9.31-5.22 12.67l-.48.48c-3.33 3.37-7.86 5.25-12.58 5.25h-106.71c-9.83 0-17.81 8.03-17.81 17.92v115.41c0 9.9 7.98 17.92 17.81 17.92h114.58c9.83 0 17.8-8.03 17.8-17.92v-108.05c0-4.41 1.74-8.63 4.83-11.75 3.09-3.11 7.28-4.86 11.66-4.86h108.5c9.83 0 17.8-8.02 17.8-17.92v-115.41c0-9.9-7.97-17.92-17.8-17.92h-114.58c-9.83 0-17.8 8.03-17.8 17.92zm34.77-99.61h80.62c5.74 0 10.39 4.69 10.39 10.46v81.19c0 5.77-4.64 10.46-10.39 10.46h-80.62c-5.73 0-10.39-4.69-10.39-10.46v-81.19c0-5.78 4.66-10.46 10.39-10.46z'
fill='#33C482'
fillRule='evenodd'
/>
@@ -101,9 +53,9 @@ export function SimLogoFull({ className }: SimLogoProps) {
</g>
{/* "Sim" text — adapts to light/dark mode */}
<g className='fill-neutral-900 dark:fill-white'>
<path d='M31.5718 15.845h2.5865c0 .7141.2586 1.2835.7759 1.7081.5173.4053 1.2166.608 2.0979.608.958 0 1.6956-.1834 2.2129-.5501.5173-.386.776-.8975.776-1.5344 0-.4632-.1437-.8492-.4311-1.158-.2682-.3088-.7664-.5597-1.4944-.7527l-2.4716-.579c-1.2453-.3088-2.1745-.7817-2.7876-1.4186-.594-.6369-.8909-1.4765-.8909-2.51873 0-.86852.2203-1.62124.661-2.25815.4598-.63692 1.0825-1.12908 1.868-1.47648.8047-.34741 1.7243-.52112 2.7589-.52112s1.9255.18336 2.6727.55007c.7664.3667 1.3603.87817 1.7818 1.53438.4407.65622.6706 1.43788.6898 2.345h-2.5865c-.0192-.73341-.2587-1.30278-.7185-1.70809-.4598-.4053-1.1017-.60796-1.9255-.60796-.843 0-1.4944.18336-1.9542.55006-.4599.36671-.6898.86852-.6898 1.50544 0 .94568.6898 1.59228 2.0692 1.93968l2.4716.608c1.1878.2702 2.0787.7141 2.6727 1.3317.5939.5983.8909 1.4186.8909 2.4608 0 .8878-.2395 1.6695-.7185 2.345-.479.6562-1.14 1.1677-1.983 1.5344-.8238.3474-1.8009.5211-2.9313.5211-1.6477 0-2.9601-.4053-3.9372-1.2159-.9772-.8106-1.4657-1.8915-1.4657-3.2425z' />
<path d='M44.5096 19.956v-14.15687c1.0772.39383 1.5521.39383 2.7014 0v14.15687zm1.322-15.09268c-.479 0-.9005-.1737-1.2645-.52111-.3449-.36671-.5173-.79132-.5173-1.27383 0-.50181.1724-.92642.5173-1.27383.364-.34741.7855-.52111 1.2645-.52111.4981 0 .9196.1737 1.2645.52111s.5173.77202.5173 1.27383c0 .48251-.1724.90712-.5173 1.27383-.3449.34741-.7664.52111-1.2645.52111z' />
<path d='M51.976 19.956h-2.7014v-14.15687h2.4141v2.38865c.2873-.79131.843-1.46223 1.6093-1.98334.7855-.54041 1.7339-.81062 2.8452-.81062 1.2453 0 2.2799.33776 3.1038 1.01328.8238.67551 1.3603 1.57298 1.6093 2.69241h-.4885c.1916-1.11943.7184-2.0169 1.5806-2.69241.8622-.67552 1.9255-1.01328 3.19-1.01328 1.6094 0 2.8739.47286 3.7935 1.41858.9197.94573 1.3795 2.23886 1.3795 3.8794v9.2642h-2.644v-8.5983c0-1.1195-.2874-1.97834-.8621-2.57665-.5557-.61761-1.3125-.92642-2.2704-.92642-.6706 0-1.2645.1544-1.7818.46321-.4982.28951-.8909.71412-1.1783 1.27383-.2874.55973-.4311 1.21593-.4311 1.96863v8.3957h-2.6727v-8.6273c0-1.1194-.2778-1.96864-.8334-2.54765-.5556-.59831-1.3124-.89747-2.2704-.89747-.6706 0-1.2645.1544-1.7818.46321-.4981.28951-.8909.71412-1.1783 1.27383-.2874.54038-.4311 1.18698-.4311 1.93968z' />
<path d='M31.57 15.85h2.59c0 .71.26 1.28.78 1.71.52.41 1.22.61 2.1.61.96 0 1.7-.18 2.21-.55.52-.39.78-.9.78-1.53 0-.46-.14-.85-.43-1.16-.27-.31-.77-.56-1.49-.75l-2.47-.58c-1.25-.31-2.17-.78-2.79-1.42-.59-.64-.89-1.48-.89-2.52 0-.87.22-1.62.66-2.26.46-.64 1.08-1.13 1.87-1.48.8-.35 1.72-.52 2.76-.52s1.93.18 2.67.55c.77.37 1.36.88 1.78 1.53.44.66.67 1.44.69 2.35h-2.59c-.02-.73-.26-1.3-.72-1.71-.46-.41-1.1-.61-1.93-.61-.84 0-1.49.18-1.95.55-.46.37-.69.87-.69 1.51 0 .95.69 1.59 2.07 1.94l2.47.61c1.19.27 2.08.71 2.67 1.33.59.6.89 1.42.89 2.46 0 .89-.24 1.67-.72 2.35-.48.66-1.14 1.17-1.98 1.53-.82.35-1.8.52-2.93.52-1.65 0-2.96-.41-3.94-1.22-.98-.81-1.47-1.89-1.47-3.24z' />
<path d='M44.51 19.96v-14.16c1.08.39 1.55.39 2.7 0v14.16zm1.32-15.09c-.48 0-.9-.17-1.26-.52-.34-.37-.52-.79-.52-1.27 0-.5.17-.93.52-1.27.36-.35.79-.52 1.26-.52.5 0 .92.17 1.26.52s.52.77.52 1.27c0 .48-.17.91-.52 1.27-.34.35-.77.52-1.26.52z' />
<path d='M51.98 19.96h-2.7v-14.16h2.41v2.39c.29-.79.84-1.46 1.61-1.98.79-.54 1.73-.81 2.85-.81 1.25 0 2.28.34 3.1 1.01.82.68 1.36 1.57 1.61 2.69h-.49c.19-1.12.72-2.02 1.58-2.69.86-.68 1.93-1.01 3.19-1.01 1.61 0 2.87.47 3.79 1.42.92.95 1.38 2.24 1.38 3.88v9.26h-2.64v-8.6c0-1.12-.29-1.98-.86-2.58-.56-.62-1.31-.93-2.27-.93-.67 0-1.26.15-1.78.46-.5.29-.89.71-1.18 1.27-.29.56-.43 1.22-.43 1.97v8.4h-2.67v-8.63c0-1.12-.28-1.97-.83-2.55-.56-.6-1.31-.9-2.27-.9-.67 0-1.26.15-1.78.46-.5.29-.89.71-1.18 1.27-.29.54-.43 1.19-.43 1.94z' />
</g>
</svg>
)
+6 -18
View File
@@ -1,7 +1,6 @@
'use client'
import type { SVGProps } from 'react'
import { useEffect, useState } from 'react'
import { useTheme } from 'next-themes'
function SunIcon(props: SVGProps<SVGSVGElement>) {
@@ -51,28 +50,17 @@ function MoonIcon(props: SVGProps<SVGSVGElement>) {
}
export function ThemeToggle() {
const { theme, setTheme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return (
<button className='flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded-full text-foreground/40'>
<MoonIcon />
</button>
)
}
const { resolvedTheme, setTheme } = useTheme()
return (
<button
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
className='flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded-full text-foreground/40 transition-colors duration-200 hover:bg-neutral-100 hover:text-foreground/70 dark:hover:bg-neutral-800 dark:hover:text-foreground/70'
type='button'
onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}
className='flex size-[30px] cursor-pointer items-center justify-center rounded-full text-foreground/40 transition-colors duration-200 hover:bg-neutral-100 hover:text-foreground/70 dark:hover:bg-neutral-800 dark:hover:text-foreground/70'
aria-label='Toggle theme'
>
{theme === 'dark' ? <MoonIcon /> : <SunIcon />}
<SunIcon className='block dark:hidden' />
<MoonIcon className='hidden dark:block' />
</button>
)
}
+29 -20
View File
@@ -31,30 +31,39 @@ export function Video({
const startTimeRef = useRef(0)
const [isLightboxOpen, setIsLightboxOpen] = useState(false)
const handleVideoClick = () => {
if (enableLightbox) {
startTimeRef.current = videoRef.current?.currentTime ?? 0
setIsLightboxOpen(true)
}
const openLightbox = () => {
startTimeRef.current = videoRef.current?.currentTime ?? 0
setIsLightboxOpen(true)
}
const video = (
<video
ref={videoRef}
autoPlay={autoPlay}
loop={loop}
muted={muted}
playsInline={playsInline}
width={width}
height={height}
className={cn(className, enableLightbox && 'transition-opacity group-hover:opacity-[0.97]')}
src={getAssetUrl(src)}
/>
)
return (
<>
<video
ref={videoRef}
autoPlay={autoPlay}
loop={loop}
muted={muted}
playsInline={playsInline}
width={width}
height={height}
className={cn(
className,
enableLightbox && 'cursor-pointer transition-opacity hover:opacity-[0.97]'
)}
src={getAssetUrl(src)}
onClick={handleVideoClick}
/>
{enableLightbox ? (
<button
type='button'
onClick={openLightbox}
aria-label={`Open ${src} in media viewer`}
className='group block w-full cursor-pointer rounded-xl p-0 text-left'
>
{video}
</button>
) : (
video
)}
{enableLightbox && (
<Lightbox
+4 -9
View File
@@ -53,16 +53,11 @@ Returns companies matching a set of criteria using Hunter.io AI-powered search.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `results` | array | List of companies matching the search criteria |
| ↳ `name` | string | Company name |
| ↳ `domain` | string | Company domain |
| ↳ `logo` | string | URL of the company logo |
| ↳ `linkedin_url` | string | LinkedIn profile URL of the company |
| ↳ `company_type` | string | Company type \(e.g., privately held, public company\) |
| ↳ `industry` | string | Industry of the company |
| ↳ `size` | string | Headcount range of the company |
| ↳ `location` | string | Headquarters location |
| ↳ `founded_year` | number | Year the company was founded |
| ↳ `crunchbase_url` | string | Crunchbase URL of the company |
| ↳ `organization` | string | Organization name |
| ↳ `personal_emails` | number | Count of personal emails |
| ↳ `generic_emails` | number | Count of generic \(role-based\) emails |
| ↳ `total_emails` | number | Total emails found for the company |
### `hunter_domain_search`
+3
View File
@@ -0,0 +1,3 @@
export function serializeJsonLd(value: unknown): string {
return JSON.stringify(value).replace(/</g, '\\u003c')
}
+1 -3
View File
@@ -1,5 +1,5 @@
import { createElement, Fragment } from 'react'
import { type InferPageType, loader, multiple } from 'fumadocs-core/source'
import { loader, multiple } from 'fumadocs-core/source'
import type { DocData, DocMethods } from 'fumadocs-mdx/runtime/types'
import { openapiSource } from 'fumadocs-openapi/server'
import { docs } from '@/.source/server'
@@ -99,5 +99,3 @@ export type PageData = DocData &
description?: string
full?: boolean
}
export type Page = InferPageType<typeof source>
@@ -1,6 +1,6 @@
'use client'
import { type ReactNode, useEffect, useState } from 'react'
import { type ReactNode, useState } from 'react'
import { Button } from '@/components/emcn'
import { GithubIcon, GoogleIcon } from '@/components/icons'
import { client } from '@/lib/auth/auth-client'
@@ -22,15 +22,6 @@ export function SocialLoginButtons({
}: SocialLoginButtonsProps) {
const [isGithubLoading, setIsGithubLoading] = useState(false)
const [isGoogleLoading, setIsGoogleLoading] = useState(false)
const [mounted, setMounted] = useState(false)
// Set mounted state to true on client-side
useEffect(() => {
setMounted(true)
}, [])
// Only render on the client side to avoid hydration errors
if (!mounted) return null
async function signInWithGithub() {
if (!githubAvailable) return
+8 -8
View File
@@ -384,8 +384,8 @@ export default function LoginPage({
/>
{showEmailValidationError && emailErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{emailErrors.map((error, index) => (
<p key={index}>{error}</p>
{emailErrors.map((error) => (
<p key={error}>{error}</p>
))}
</div>
)}
@@ -431,8 +431,8 @@ export default function LoginPage({
</div>
{showValidationError && passwordErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{passwordErrors.map((error, index) => (
<p key={index}>{error}</p>
{passwordErrors.map((error) => (
<p key={error}>{error}</p>
))}
</div>
)}
@@ -454,8 +454,8 @@ export default function LoginPage({
<button type='submit' disabled={isLoading} className={AUTH_SUBMIT_BTN}>
{isLoading ? (
<span className='flex items-center gap-2'>
<Loader className='h-4 w-4' animate />
Signing in...
<Loader className='size-4' animate />
Signing in
</span>
) : (
'Sign in'
@@ -569,8 +569,8 @@ export default function LoginPage({
<button type='submit' disabled={isSubmittingReset} className={AUTH_SUBMIT_BTN}>
{isSubmittingReset ? (
<span className='flex items-center gap-2'>
<Loader className='h-4 w-4' animate />
Sending...
<Loader className='size-4' animate />
Sending
</span>
) : (
'Send Reset Link'
+8 -5
View File
@@ -1,3 +1,4 @@
import { Suspense } from 'react'
import type { Metadata } from 'next'
import { getOAuthProviderStatus } from '@/app/(auth)/components/oauth-provider-checker'
import LoginForm from '@/app/(auth)/login/login-form'
@@ -12,10 +13,12 @@ export default async function LoginPage() {
const { githubAvailable, googleAvailable, isProduction } = await getOAuthProviderStatus()
return (
<LoginForm
githubAvailable={githubAvailable}
googleAvailable={googleAvailable}
isProduction={isProduction}
/>
<Suspense fallback={null}>
<LoginForm
githubAvailable={githubAvailable}
googleAvailable={googleAvailable}
isProduction={isProduction}
/>
</Suspense>
)
}
@@ -4,9 +4,9 @@ export default function OAuthConsentLoading() {
return (
<div className='flex flex-col items-center'>
<div className='flex items-center gap-4'>
<Skeleton className='h-[48px] w-[48px] rounded-[12px]' />
<Skeleton className='h-[20px] w-[20px] rounded-[4px]' />
<Skeleton className='h-[48px] w-[48px] rounded-[12px]' />
<Skeleton className='size-[48px] rounded-[12px]' />
<Skeleton className='size-[20px] rounded-[4px]' />
<Skeleton className='size-[48px] rounded-[12px]' />
</div>
<Skeleton className='mt-6 h-[38px] w-[220px] rounded-[4px]' />
<Skeleton className='mt-2 h-[14px] w-[280px] rounded-[4px]' />
+6 -6
View File
@@ -143,7 +143,7 @@ function OAuthConsentInner() {
Authorize Application
</h1>
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-lg leading-[125%] tracking-[0.02em]'>
Loading application details...
Loading application details
</p>
</div>
</div>
@@ -184,11 +184,11 @@ function OAuthConsentInner() {
className='rounded-[10px]'
/>
) : (
<div className='flex h-12 w-12 items-center justify-center rounded-[10px] bg-[var(--landing-bg-elevated)] font-medium text-[var(--landing-text-muted)] text-lg'>
<div className='flex size-12 items-center justify-center rounded-[10px] bg-[var(--landing-bg-elevated)] font-medium text-[var(--landing-text-muted)] text-lg'>
{(clientName ?? '?').charAt(0).toUpperCase()}
</div>
)}
<ArrowLeftRight className='h-5 w-5 text-[var(--landing-text-muted)]' />
<ArrowLeftRight className='size-5 text-[var(--landing-text-muted)]' />
<Image
src='/new/logo/colorized-bg.svg'
alt='Sim'
@@ -220,7 +220,7 @@ function OAuthConsentInner() {
unoptimized
/>
) : (
<div className='flex h-8 w-8 items-center justify-center rounded-full bg-[var(--landing-bg-elevated)] font-medium text-[var(--landing-text-muted)] text-small'>
<div className='flex size-8 items-center justify-center rounded-full bg-[var(--landing-bg-elevated)] font-medium text-[var(--landing-text-muted)] text-small'>
{(session.user.name ?? session.user.email ?? '?').charAt(0).toUpperCase()}
</div>
)}
@@ -278,8 +278,8 @@ function OAuthConsentInner() {
>
{submitting ? (
<span className='flex items-center gap-2'>
<Loader className='h-4 w-4' animate />
Authorizing...
<Loader className='size-4' animate />
Authorizing
</span>
) : (
'Allow'
@@ -95,9 +95,7 @@ function ResetPasswordContent() {
export default function ResetPasswordPage() {
return (
<Suspense
fallback={<div className='flex h-screen items-center justify-center'>Loading...</div>}
>
<Suspense fallback={<div className='flex h-screen items-center justify-center'>Loading</div>}>
<ResetPasswordContent />
</Suspense>
)
@@ -67,8 +67,8 @@ export function RequestResetForm({
<button type='submit' disabled={isSubmitting} className={AUTH_SUBMIT_BTN}>
{isSubmitting ? (
<span className='flex items-center gap-2'>
<Loader className='h-4 w-4' animate />
Sending...
<Loader className='size-4' animate />
Sending
</span>
) : (
'Send Reset Link'
@@ -211,8 +211,8 @@ export function SetNewPasswordForm({
{validationMessages.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{validationMessages.map((error, index) => (
<p key={index}>{error}</p>
{validationMessages.map((error) => (
<p key={error}>{error}</p>
))}
</div>
)}
@@ -232,8 +232,8 @@ export function SetNewPasswordForm({
<button type='submit' disabled={isSubmitting || !token} className={AUTH_SUBMIT_BTN}>
{isSubmitting ? (
<span className='flex items-center gap-2'>
<Loader className='h-4 w-4' animate />
Resetting...
<Loader className='size-4' animate />
Resetting
</span>
) : (
'Reset Password'
+8 -10
View File
@@ -406,8 +406,8 @@ function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: S
>
<div className='overflow-hidden'>
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{nameErrors.map((error, index) => (
<p key={index}>{error}</p>
{nameErrors.map((error) => (
<p key={error}>{error}</p>
))}
</div>
</div>
@@ -451,7 +451,7 @@ function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: S
<div className='overflow-hidden'>
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{showEmailValidationError && emailErrors.length > 0 ? (
emailErrors.map((error, index) => <p key={index}>{error}</p>)
emailErrors.map((error) => <p key={error}>{error}</p>)
) : emailError && !showEmailValidationError ? (
<p>{emailError}</p>
) : null}
@@ -503,8 +503,8 @@ function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: S
>
<div className='overflow-hidden'>
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{passwordErrors.map((error, index) => (
<p key={index}>{error}</p>
{passwordErrors.map((error) => (
<p key={error}>{error}</p>
))}
</div>
</div>
@@ -530,8 +530,8 @@ function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: S
<button type='submit' disabled={isLoading} className={cn('!mt-6', AUTH_SUBMIT_BTN)}>
{isLoading ? (
<span className='flex items-center gap-2'>
<Loader className='h-4 w-4' animate />
Creating account...
<Loader className='size-4' animate />
Creating account
</span>
) : (
'Create account'
@@ -628,9 +628,7 @@ export default function SignupPage({
isProduction,
}: SignupFormProps) {
return (
<Suspense
fallback={<div className='flex h-screen items-center justify-center'>Loading...</div>}
>
<Suspense fallback={<div className='flex h-screen items-center justify-center'>Loading</div>}>
<SignupFormContent
githubAvailable={githubAvailable}
googleAvailable={googleAvailable}
+6 -1
View File
@@ -1,3 +1,4 @@
import { Suspense } from 'react'
import type { Metadata } from 'next'
import { redirect } from 'next/navigation'
import { getEnv, isTruthy } from '@/lib/core/config/env'
@@ -14,5 +15,9 @@ export default async function SSOPage() {
redirect('/login')
}
return <SSOForm />
return (
<Suspense fallback={null}>
<SSOForm />
</Suspense>
)
}
@@ -117,8 +117,8 @@ function VerificationForm({
>
{isLoading ? (
<span className='flex items-center gap-2'>
<Loader className='h-4 w-4' animate />
Verifying...
<Loader className='size-4' animate />
Verifying
</span>
) : (
'Verify Email'
@@ -9,7 +9,7 @@ export function BackLink() {
className='group/link inline-flex items-center gap-1.5 font-season text-[var(--landing-text-muted)] text-sm tracking-[0.02em] hover:text-[var(--landing-text)]'
>
<svg
className='h-3 w-3 shrink-0'
className='size-3 shrink-0'
viewBox='0 0 10 10'
fill='none'
xmlns='http://www.w3.org/2000/svg'
@@ -21,7 +21,7 @@ export default function BlogPostLoading() {
<div className='mt-6 flex items-center gap-6'>
<Skeleton className='h-[12px] w-[100px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
<div className='flex items-center gap-2'>
<Skeleton className='h-[20px] w-[20px] rounded-full bg-[var(--landing-bg-elevated)]' />
<Skeleton className='size-[20px] rounded-full bg-[var(--landing-bg-elevated)]' />
<Skeleton className='h-[12px] w-[80px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
</div>
</div>
+2 -2
View File
@@ -92,8 +92,8 @@ export default async function Page({ params }: { params: Promise<{ slug: string
</time>
<meta itemProp='dateModified' content={post.updated ?? post.date} />
<div className='flex items-center gap-3'>
{(post.authors || [post.author]).map((a, idx) => (
<div key={idx} className='flex items-center gap-2'>
{(post.authors || [post.author]).map((a) => (
<div key={a?.name} className='flex items-center gap-2'>
{a?.avatarUrl ? (
<Avatar className='size-5'>
<AvatarImage src={a.avatarUrl} alt={a.name} />
@@ -46,21 +46,21 @@ export function ShareButton({ url, title }: ShareButtonProps) {
className='flex items-center gap-1.5 text-[var(--landing-text-muted)] text-sm hover:text-[var(--landing-text)]'
aria-label='Share this post'
>
<Share2 className='h-4 w-4' />
<Share2 className='size-4' />
<span>Share</span>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem onSelect={handleCopyLink}>
<Copy className='h-4 w-4' />
<Copy className='size-4' />
{copied ? 'Copied!' : 'Copy link'}
</DropdownMenuItem>
<DropdownMenuItem onSelect={handleShareTwitter}>
<XIcon className='h-4 w-4' />
<XIcon className='size-4' />
Share on X
</DropdownMenuItem>
<DropdownMenuItem onSelect={handleShareLinkedIn}>
<LinkedInIcon className='h-4 w-4' />
<LinkedInIcon className='size-4' />
Share on LinkedIn
</DropdownMenuItem>
</DropdownMenuContent>
@@ -6,7 +6,7 @@ export default function AuthorLoading() {
return (
<main className='mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12'>
<div className='mb-6 flex items-center gap-3'>
<Skeleton className='h-[40px] w-[40px] rounded-full bg-[var(--landing-bg-elevated)]' />
<Skeleton className='size-[40px] rounded-full bg-[var(--landing-bg-elevated)]' />
<Skeleton className='h-[32px] w-[160px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
</div>
<div className='grid grid-cols-1 gap-8 sm:grid-cols-2'>
@@ -1,6 +1,6 @@
'use client'
import { useEffect, useRef } from 'react'
import { useEffect, useEffectEvent, useRef } from 'react'
interface LightboxProps {
isOpen: boolean
@@ -12,18 +12,20 @@ interface LightboxProps {
export function Lightbox({ isOpen, onClose, src, alt }: LightboxProps) {
const overlayRef = useRef<HTMLDivElement>(null)
const onCloseEvent = useEffectEvent(onClose)
useEffect(() => {
if (!isOpen) return
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose()
onCloseEvent()
}
}
const handleClickOutside = (event: MouseEvent) => {
if (overlayRef.current && event.target === overlayRef.current) {
onClose()
onCloseEvent()
}
}
@@ -36,7 +38,7 @@ export function Lightbox({ isOpen, onClose, src, alt }: LightboxProps) {
document.removeEventListener('click', handleClickOutside)
document.body.style.overflow = 'unset'
}
}, [isOpen, onClose])
}, [isOpen])
if (!isOpen) return null
@@ -49,13 +51,14 @@ export function Lightbox({ isOpen, onClose, src, alt }: LightboxProps) {
aria-label='Image viewer'
>
<div className='relative max-h-full max-w-full overflow-hidden rounded-xl shadow-2xl'>
<img
src={src}
alt={alt}
className='max-h-[75vh] max-w-[75vw] cursor-pointer rounded-xl object-contain'
loading='lazy'
onClick={onClose}
/>
<button type='button' className='block cursor-pointer rounded-xl' onClick={onClose}>
<img
src={src}
alt={alt}
className='max-h-[75vh] max-w-[75vw] rounded-xl object-contain'
loading='lazy'
/>
</button>
</div>
</div>
)
+1 -1
View File
@@ -38,7 +38,7 @@ export default function BlogLoading() {
{/* List skeleton */}
{Array.from({ length: 5 }).map((_, i) => (
<div key={i}>
<div className='flex items-center gap-6 px-6 py-6'>
<div className='flex items-center gap-6 p-6'>
<Skeleton className='hidden h-[14px] w-[120px] rounded-[4px] bg-[var(--landing-bg-elevated)] md:block' />
<div className='flex min-w-0 flex-1 flex-col gap-1'>
<Skeleton className='h-[18px] w-[70%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
+1 -1
View File
@@ -169,7 +169,7 @@ export default async function BlogIndex({
<div key={p.slug}>
<Link
href={`/blog/${p.slug}`}
className='group flex items-start gap-6 px-6 py-6 transition-colors hover:bg-[var(--landing-bg-elevated)] md:items-center'
className='group flex items-start gap-6 p-6 transition-colors hover:bg-[var(--landing-bg-elevated)] md:items-center'
>
{/* Date */}
<span className='hidden w-[120px] shrink-0 pt-1 font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em] md:block'>
@@ -137,13 +137,13 @@ export function AuthModal({ children, defaultView = 'login', source }: AuthModal
<div className='relative px-6 pt-6 pb-6'>
<ModalClose className='absolute top-6 right-6 rounded-sm opacity-70 transition-opacity hover:opacity-100'>
<X className='h-5 w-5 text-[var(--landing-text-muted)]' />
<X className='size-5 text-[var(--landing-text-muted)]' />
<span className='sr-only'>Close</span>
</ModalClose>
{!providerStatus ? (
<div className='flex items-center justify-center py-16'>
<Loader className='h-5 w-5 text-[var(--landing-text-muted)]' animate />
<Loader className='size-5 text-[var(--landing-text-muted)]' animate />
</div>
) : (
<>
@@ -174,7 +174,7 @@ export function AuthModal({ children, defaultView = 'login', source }: AuthModal
disabled={!!socialLoading}
className={SOCIAL_BTN}
>
<GoogleIcon className='absolute left-4 h-[18px] w-[18px] shrink-0' />
<GoogleIcon className='absolute left-4 size-[18px] shrink-0' />
<span>
{socialLoading === 'google' ? 'Connecting...' : 'Continue with Google'}
</span>
@@ -187,7 +187,7 @@ export function AuthModal({ children, defaultView = 'login', source }: AuthModal
disabled={!!socialLoading}
className={SOCIAL_BTN}
>
<GithubIcon className='absolute left-4 h-[18px] w-[18px] shrink-0' />
<GithubIcon className='absolute left-4 size-[18px] shrink-0' />
<span>
{socialLoading === 'github' ? 'Connecting...' : 'Continue with GitHub'}
</span>
@@ -32,7 +32,7 @@ function DotGrid({ className, cols, rows, gap = 0 }: DotGridProps) {
}}
>
{Array.from({ length: cols * rows }, (_, i) => (
<div key={i} className='h-[1.5px] w-[1.5px] rounded-full bg-[var(--landing-bg-elevated)]' />
<div key={i} className='size-[1.5px] rounded-full bg-[var(--landing-bg-elevated)]' />
))}
</div>
)
@@ -72,6 +72,15 @@ const CURSOR_ARROW_PATH =
const CURSOR_ARROW_MIRRORED_PATH =
'M0.365 2.198L4.522 14.821C5.022 16.339 7.225 16.16 7.472 14.58L8.394 8.702C8.49 8.091 8.946 7.599 9.548 7.456L15.909 5.953C17.5 5.577 17.461 3.299 15.857 2.978L2.11 0.228C0.966 0 0.001 1.09 0.365 2.198Z'
// Long-running decorative loops for the landing visual, not UI feedback transitions.
const AMBIENT_CURSOR_ANIMATION_SECONDS = {
vikhyath: 16,
alexa: 13,
} as const
const getAmbientCursorAnimation = (name: string, durationSeconds: number) =>
`${name} ${durationSeconds}s ease-in-out infinite`
function CursorArrow({ fill }: { fill: string }) {
return (
<svg width='23.15' height='21.1' viewBox='0 0 17.5 16.4' fill='none'>
@@ -88,8 +97,10 @@ function VikhyathCursor() {
style={{
top: '27.47%',
left: '25%',
animation: 'cursorVikhyath 16s ease-in-out infinite',
willChange: 'transform',
animation: getAmbientCursorAnimation(
'cursorVikhyath',
AMBIENT_CURSOR_ANIMATION_SECONDS.vikhyath
),
}}
>
<div className='relative h-[37.14px] w-[79.18px]'>
@@ -112,8 +123,7 @@ function AlexaCursor() {
style={{
top: '66.80%',
left: '49%',
animation: 'cursorAlexa 13s ease-in-out infinite',
willChange: 'transform',
animation: getAmbientCursorAnimation('cursorAlexa', AMBIENT_CURSOR_ANIMATION_SECONDS.alexa),
}}
>
<div className='relative h-[35.09px] w-[62.16px]'>
@@ -282,7 +292,7 @@ export default function Collaboration() {
>
Build together
<svg
className='h-[10px] w-[10px] shrink-0'
className='size-[10px] shrink-0'
viewBox='0 0 10 10'
fill='none'
xmlns='http://www.w3.org/2000/svg'
@@ -339,7 +349,13 @@ export default function Collaboration() {
className='relative mx-4 mb-6 flex cursor-none items-center gap-3.5 rounded-[5px] border border-[var(--landing-bg-elevated)] bg-[var(--landing-bg)] px-3 py-2.5 transition-colors hover:border-[var(--landing-border-strong)] hover:bg-[var(--landing-bg-card)] sm:mx-8 md:absolute md:bottom-10 md:left-16 md:z-20 md:mx-0 md:mb-0'
>
<div className='relative h-7 w-11 shrink-0'>
<Image src='/landing/multiplayer-cursors.svg' alt='' fill className='object-contain' />
<Image
src='/landing/multiplayer-cursors.svg'
alt=''
fill
sizes='44px'
className='object-contain'
/>
</div>
<div className='flex flex-col gap-0.5'>
<span className='font-[430] font-season text-[#F6F6F0]/50 text-caption uppercase leading-[100%] tracking-[0.08em]'>
@@ -149,8 +149,8 @@ export function ContactForm() {
if (submitSuccess) {
return (
<div className='flex flex-col items-center px-8 py-16 text-center'>
<div className='flex h-16 w-16 items-center justify-center rounded-full border border-[var(--landing-bg-elevated)] bg-[var(--landing-bg-surface)] text-[var(--landing-text)]'>
<Check className='h-8 w-8' />
<div className='flex size-16 items-center justify-center rounded-full border border-[var(--landing-bg-elevated)] bg-[var(--landing-bg-surface)] text-[var(--landing-text)]'>
<Check className='size-8' />
</div>
<h2 className='mt-6 font-[430] font-season text-[24px] text-[var(--landing-text)] leading-[1.2] tracking-[-0.02em]'>
Message received
@@ -251,8 +251,8 @@ export function DemoRequestModal({ children, theme = 'dark' }: DemoRequestModalP
{submitSuccess ? (
<div className='absolute inset-0 flex items-center justify-center px-8 pb-10 sm:px-12 sm:pb-14'>
<div className='flex max-w-md flex-col items-center justify-center text-center'>
<div className='flex h-20 w-20 items-center justify-center rounded-full border border-[var(--border)] bg-[var(--bg-subtle)] text-[var(--text-primary)]'>
<Check className='h-10 w-10' />
<div className='flex size-20 items-center justify-center rounded-full border border-[var(--border)] bg-[var(--bg-subtle)] text-[var(--text-primary)]'>
<Check className='size-10' />
</div>
<h2 className='mt-8 font-[430] font-season text-[34px] text-[var(--text-primary)] leading-[1.1] tracking-[-0.03em]'>
{SUBMIT_SUCCESS_MESSAGE}
@@ -1,212 +0,0 @@
'use client'
import { useRef, useState } from 'react'
import { motion, useInView } from 'framer-motion'
import { PROVIDER_DEFINITIONS } from '@/providers/models'
interface PermissionFeature {
name: string
key: string
defaultEnabled: boolean
providerId?: string
}
interface PermissionCategory {
label: string
color: string
features: PermissionFeature[]
}
const PERMISSION_CATEGORIES: PermissionCategory[] = [
{
label: 'Providers',
color: '#FA4EDF',
features: [
{ key: 'openai', name: 'OpenAI', defaultEnabled: true, providerId: 'openai' },
{ key: 'anthropic', name: 'Anthropic', defaultEnabled: true, providerId: 'anthropic' },
{ key: 'google', name: 'Google', defaultEnabled: false, providerId: 'google' },
{ key: 'xai', name: 'xAI', defaultEnabled: true, providerId: 'xai' },
],
},
{
label: 'Workspace',
color: '#2ABBF8',
features: [
{ key: 'knowledge-base', name: 'Knowledge Base', defaultEnabled: true },
{ key: 'tables', name: 'Tables', defaultEnabled: true },
{ key: 'copilot', name: 'Copilot', defaultEnabled: false },
{ key: 'environment', name: 'Environment', defaultEnabled: false },
],
},
{
label: 'Tools',
color: '#33C482',
features: [
{ key: 'mcp-tools', name: 'MCP Tools', defaultEnabled: true },
{ key: 'custom-tools', name: 'Custom Tools', defaultEnabled: false },
{ key: 'skills', name: 'Skills', defaultEnabled: true },
{ key: 'invitations', name: 'Invitations', defaultEnabled: true },
],
},
]
const INITIAL_ACCESS_STATE = Object.fromEntries(
PERMISSION_CATEGORIES.flatMap((category) =>
category.features.map((feature) => [feature.key, feature.defaultEnabled])
)
)
function CheckboxIcon({ checked, color }: { checked: boolean; color: string }) {
return (
<div
className='h-[6px] w-[6px] shrink-0 rounded-full transition-colors duration-200'
style={{
backgroundColor: checked ? color : 'transparent',
border: checked ? 'none' : '1.5px solid #3A3A3A',
}}
/>
)
}
function ProviderPreviewIcon({ providerId }: { providerId?: string }) {
if (!providerId) return null
const ProviderIcon = PROVIDER_DEFINITIONS[providerId]?.icon
if (!ProviderIcon) return null
return (
<div className='relative flex h-[14px] w-[14px] shrink-0 items-center justify-center opacity-50 brightness-0 invert'>
<ProviderIcon className='!h-[14px] !w-[14px]' />
</div>
)
}
interface FeatureToggleItemProps {
feature: PermissionFeature
enabled: boolean
color: string
isInView: boolean
delay: number
textClassName: string
transition: Record<string, unknown>
onToggle: () => void
}
function FeatureToggleItem({
feature,
enabled,
color,
isInView,
delay,
textClassName,
transition,
onToggle,
}: FeatureToggleItemProps) {
return (
<motion.div
key={feature.key}
role='button'
tabIndex={0}
aria-label={`Toggle ${feature.name}`}
aria-pressed={enabled}
className='flex cursor-pointer items-center gap-2 rounded-[4px] py-0.5'
initial={{ opacity: 0, x: -6 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ ...transition, delay }}
onClick={onToggle}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
onToggle()
}
}}
whileTap={{ scale: 0.98 }}
>
<CheckboxIcon checked={enabled} color={color} />
<ProviderPreviewIcon providerId={feature.providerId} />
<span className={textClassName} style={{ color: enabled ? '#F6F6F6AA' : '#F6F6F640' }}>
{feature.name}
</span>
</motion.div>
)
}
export function AccessControlPanel() {
const ref = useRef(null)
const isInView = useInView(ref, { once: true, margin: '-40px' })
const [accessState, setAccessState] = useState<Record<string, boolean>>(INITIAL_ACCESS_STATE)
return (
<div ref={ref}>
<div className='lg:hidden'>
{PERMISSION_CATEGORIES.map((category, catIdx) => {
const offsetBefore = PERMISSION_CATEGORIES.slice(0, catIdx).reduce(
(sum, c) => sum + c.features.length,
0
)
return (
<div key={category.label} className={catIdx > 0 ? 'mt-4' : ''}>
<span className='font-[430] font-season text-[#F6F6F6]/55 text-[10px] uppercase leading-none tracking-[0.08em]'>
{category.label}
</span>
<div className='mt-2 grid grid-cols-2 gap-x-4 gap-y-2'>
{category.features.map((feature, featIdx) => (
<FeatureToggleItem
key={feature.key}
feature={feature}
enabled={accessState[feature.key]}
color={category.color}
isInView={isInView}
delay={0.05 + (offsetBefore + featIdx) * 0.04}
textClassName='truncate font-[430] font-season text-[13px] leading-none tracking-[0.02em]'
transition={{ duration: 0.3 }}
onToggle={() =>
setAccessState((prev) => ({ ...prev, [feature.key]: !prev[feature.key] }))
}
/>
))}
</div>
</div>
)
})}
</div>
{/* Desktop -- categorized grid */}
<div className='hidden lg:block'>
{PERMISSION_CATEGORIES.map((category, catIdx) => (
<div key={category.label} className={catIdx > 0 ? 'mt-4' : ''}>
<span className='font-[430] font-season text-[#F6F6F6]/55 text-[10px] uppercase leading-none tracking-[0.08em]'>
{category.label}
</span>
<div className='mt-2 grid grid-cols-2 gap-x-4 gap-y-2'>
{category.features.map((feature, featIdx) => {
const currentIndex =
PERMISSION_CATEGORIES.slice(0, catIdx).reduce(
(sum, c) => sum + c.features.length,
0
) + featIdx
return (
<FeatureToggleItem
key={feature.key}
feature={feature}
enabled={accessState[feature.key]}
color={category.color}
isInView={isInView}
delay={0.1 + currentIndex * 0.04}
textClassName='truncate font-[430] font-season text-[11px] leading-none tracking-[0.02em] transition-opacity duration-200'
transition={{ duration: 0.3, ease: [0.25, 0.46, 0.45, 0.94] }}
onToggle={() =>
setAccessState((prev) => ({ ...prev, [feature.key]: !prev[feature.key] }))
}
/>
)
})}
</div>
</div>
))}
</div>
</div>
)
}
@@ -1,225 +0,0 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
/** Consistent color per actor -- same pattern as Collaboration section cursors. */
const ACTOR_COLORS: Record<string, string> = {
'Sarah K.': '#2ABBF8',
'Sid G.': '#33C482',
'Theo L.': '#FA4EDF',
'Abhay K.': '#FFCC02',
'Danny S.': '#FF6B35',
}
/** Left accent bar opacity by recency -- newest is brightest. */
const ACCENT_OPACITIES = [0.75, 0.5, 0.35, 0.22, 0.12, 0.05] as const
interface LogEntry {
id: number
actor: string
/** Matches the `description` field stored by recordAudit() */
description: string
resourceType: string
/** Unix ms timestamp of when this entry was "received" */
insertedAt: number
}
function formatTimeAgo(insertedAt: number): string {
const elapsed = Date.now() - insertedAt
if (elapsed < 8_000) return 'just now'
if (elapsed < 60_000) return `${Math.floor(elapsed / 1000)}s ago`
return `${Math.floor(elapsed / 60_000)}m ago`
}
/**
* Entry templates using real description strings from the actual recordAudit()
* calls across the codebase (e.g. `Added BYOK key for openai`,
* `Invited alex@acme.com to workspace as member`).
*/
const ENTRY_TEMPLATES: Omit<LogEntry, 'id' | 'insertedAt'>[] = [
{ actor: 'Sarah K.', description: 'Deployed workflow "Email Triage"', resourceType: 'workflow' },
{
actor: 'Sid G.',
description: 'Invited alex@acme.com to workspace as member',
resourceType: 'member',
},
{ actor: 'Theo L.', description: 'Added BYOK key for openai', resourceType: 'byok_key' },
{ actor: 'Sarah K.', description: 'Created workflow "Invoice Parser"', resourceType: 'workflow' },
{
actor: 'Abhay K.',
description: 'Created permission group "Engineering"',
resourceType: 'permission_group',
},
{ actor: 'Danny S.', description: 'Created API key "Production Key"', resourceType: 'api_key' },
{
actor: 'Theo L.',
description: 'Changed permissions for sam@acme.com to editor',
resourceType: 'member',
},
{ actor: 'Sarah K.', description: 'Uploaded file "Q3_Report.pdf"', resourceType: 'file' },
{
actor: 'Sid G.',
description: 'Created credential set "Prod Keys"',
resourceType: 'credential_set',
},
{
actor: 'Abhay K.',
description: 'Created knowledge base "Internal Docs"',
resourceType: 'knowledge_base',
},
{ actor: 'Danny S.', description: 'Updated environment variables', resourceType: 'environment' },
{
actor: 'Sarah K.',
description: 'Added tool "search_web" to MCP server',
resourceType: 'mcp_server',
},
{ actor: 'Sid G.', description: 'Created webhook "Stripe Payment"', resourceType: 'webhook' },
{ actor: 'Theo L.', description: 'Deployed chat "Support Assistant"', resourceType: 'chat' },
{ actor: 'Abhay K.', description: 'Created table "Lead Tracker"', resourceType: 'table' },
{ actor: 'Danny S.', description: 'Revoked API key "Staging Key"', resourceType: 'api_key' },
{
actor: 'Sarah K.',
description: 'Duplicated workflow "Data Enrichment"',
resourceType: 'workflow',
},
{
actor: 'Sid G.',
description: 'Removed member theo@acme.com from workspace',
resourceType: 'member',
},
{
actor: 'Theo L.',
description: 'Updated knowledge base "Product Docs"',
resourceType: 'knowledge_base',
},
{ actor: 'Abhay K.', description: 'Created folder "Finance Workflows"', resourceType: 'folder' },
{
actor: 'Danny S.',
description: 'Uploaded document "onboarding-guide.pdf"',
resourceType: 'document',
},
{
actor: 'Sarah K.',
description: 'Updated credential set "Prod Keys"',
resourceType: 'credential_set',
},
{
actor: 'Sid G.',
description: 'Added member abhay@acme.com to permission group "Engineering"',
resourceType: 'permission_group',
},
{ actor: 'Theo L.', description: 'Locked workflow "Customer Sync"', resourceType: 'workflow' },
]
const INITIAL_OFFSETS_MS = [0, 20_000, 75_000, 180_000, 360_000, 600_000]
interface AuditRowProps {
entry: LogEntry
index: number
}
function AuditRow({ entry, index }: AuditRowProps) {
const color = ACTOR_COLORS[entry.actor] ?? '#F6F6F6'
const accentOpacity = ACCENT_OPACITIES[index] ?? 0.04
const timeAgo = formatTimeAgo(entry.insertedAt)
return (
<div className='group relative overflow-hidden border-[var(--landing-border)] border-b bg-[var(--landing-bg)] transition-colors duration-150 last:border-b-0 hover:bg-[#212121]'>
{/* Left accent bar -- brightness encodes recency */}
<div
aria-hidden='true'
className='absolute top-0 bottom-0 left-0 w-[2px] transition-opacity duration-150 group-hover:opacity-100'
style={{ backgroundColor: color, opacity: accentOpacity }}
/>
{/* Row content */}
<div className='flex min-w-0 items-center gap-3 py-[10px] pr-4 pl-5'>
{/* Actor avatar */}
<div
className='flex h-[22px] w-[22px] shrink-0 items-center justify-center rounded-full'
style={{ backgroundColor: `${color}20` }}
>
<span className='font-[500] font-season text-[9px] leading-none' style={{ color }}>
{entry.actor[0]}
</span>
</div>
{/* Time */}
<span className='w-[56px] shrink-0 font-[430] font-season text-[#F6F6F6]/55 text-[11px] leading-none tracking-[0.02em]'>
{timeAgo}
</span>
<span className='min-w-0 truncate font-[430] font-season text-[12px] leading-none tracking-[0.02em]'>
<span className='text-[#F6F6F6]/80'>{entry.actor}</span>
<span className='hidden sm:inline'>
<span className='text-[#F6F6F6]/60'> · </span>
<span className='text-[#F6F6F6]/55'>{entry.description}</span>
</span>
</span>
</div>
</div>
)
}
export function AuditLogPreview() {
const counterRef = useRef(ENTRY_TEMPLATES.length)
const templateIndexRef = useRef(6 % ENTRY_TEMPLATES.length)
const [entries, setEntries] = useState<LogEntry[]>(() => {
const now = Date.now()
return ENTRY_TEMPLATES.slice(0, 6).map((t, i) => ({
...t,
id: i,
insertedAt: now - INITIAL_OFFSETS_MS[i],
}))
})
const [, tick] = useState(0)
useEffect(() => {
const addInterval = setInterval(() => {
const template = ENTRY_TEMPLATES[templateIndexRef.current]
templateIndexRef.current = (templateIndexRef.current + 1) % ENTRY_TEMPLATES.length
setEntries((prev) => [
{ ...template, id: counterRef.current++, insertedAt: Date.now() },
...prev.slice(0, 5),
])
}, 2600)
// Refresh time labels every 5s so "just now" ages to "Xs ago"
const tickInterval = setInterval(() => tick((n) => n + 1), 5_000)
return () => {
clearInterval(addInterval)
clearInterval(tickInterval)
}
}, [])
return (
<div className='mt-5 overflow-hidden px-6 md:mt-6 md:px-8'>
<AnimatePresence mode='popLayout' initial={false}>
{entries.map((entry, index) => (
<motion.div
key={entry.id}
layout
initial={{ y: -48, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ opacity: 0 }}
transition={{
layout: {
type: 'tween',
duration: 0.32,
ease: [0.25, 0.46, 0.45, 0.94],
},
y: { duration: 0.32, ease: [0.25, 0.46, 0.45, 0.94] },
opacity: { duration: 0.25 },
}}
>
<AuditRow entry={entry} index={index} />
</motion.div>
))}
</AnimatePresence>
</div>
)
}
@@ -1,266 +0,0 @@
/**
* Enterprise section — compliance, scale, and security messaging.
*
* SEO:
* - `<section id="enterprise" aria-labelledby="enterprise-heading">`.
* - `<h2 id="enterprise-heading">` for the section title.
* - Compliance cert (SOC 2) as visible `<strong>` text.
* - Enterprise CTA links to contact form via `<a>` with `rel="noopener noreferrer"`.
*
* GEO:
* - Entity-rich: "Sim is SOC 2 compliant" — not "We are compliant."
* - `<ul>` checklist of features (SSO, RBAC, audit logs, SLA, on-premise deployment)
* as an atomic answer block for "What enterprise features does Sim offer?".
*/
import Image from 'next/image'
import Link from 'next/link'
import { Badge } from '@/components/emcn'
import { Lock } from '@/components/emcn/icons'
import { GithubIcon } from '@/components/icons'
import { DemoRequestModal } from '@/app/(landing)/components/demo-request/demo-request-modal'
import { AccessControlPanel } from '@/app/(landing)/components/enterprise/components/access-control-panel'
import { AuditLogPreview } from '@/app/(landing)/components/enterprise/components/audit-log-preview'
const ENTERPRISE_FEATURE_MARQUEE_STYLES = `
@keyframes enterprise-feature-marquee {
0% { transform: translateX(0); }
100% { transform: translateX(-25%); }
}
.enterprise-feature-marquee-track {
animation: enterprise-feature-marquee 30s linear infinite;
}
.enterprise-feature-marquee:hover .enterprise-feature-marquee-track {
animation-play-state: paused;
}
.enterprise-feature-marquee-tag {
transition: background-color 0.3s ease, color 0.3s ease;
}
@media (prefers-reduced-motion: reduce) {
.enterprise-feature-marquee-track {
animation: none;
}
.enterprise-feature-marquee-tag {
transition: none;
}
}
`
const FEATURE_TAGS = [
'Access Control',
'Self-Hosting',
'Bring Your Own Key',
'Credential Sharing',
'Custom Limits',
'Admin API',
'White Labeling',
'Dedicated Support',
'99.99% Uptime SLA',
'Workflow Versioning',
'On-Premise',
'Organizations',
'Workspace Export',
'Audit Logs',
] as const
function TrustStrip() {
return (
<div className='mx-6 mt-4 grid grid-cols-1 overflow-hidden rounded-lg border border-[var(--landing-bg-elevated)] sm:grid-cols-3 md:mx-8'>
{/* SOC 2 */}
<Link
href='https://app.vanta.com/sim.ai/trust/v35ia0jil4l7dteqjgaktn'
target='_blank'
rel='noopener noreferrer'
className='group flex items-center gap-3 border-[var(--landing-bg-elevated)] border-b px-4 py-3.5 transition-colors hover:bg-[#212121] sm:border-r sm:border-b-0'
>
<Image
src='/footer/soc2.png'
alt='SOC 2 Type II'
width={22}
height={22}
className='shrink-0 object-contain'
unoptimized
/>
<div className='flex flex-col gap-[3px]'>
<strong className='font-[430] font-season text-small text-white leading-none'>
SOC 2
</strong>
<span className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_55%,transparent)] text-xs leading-none tracking-[0.02em] transition-colors group-hover:text-[color-mix(in_srgb,var(--landing-text-subtle)_75%,transparent)]'>
Type II
</span>
</div>
</Link>
{/* Open Source -- center */}
<Link
href='https://github.com/simstudioai/sim'
target='_blank'
rel='noopener noreferrer'
className='group flex items-center gap-3 border-[var(--landing-bg-elevated)] border-b px-4 py-3.5 transition-colors hover:bg-[#212121] sm:border-r sm:border-b-0'
>
<div className='flex h-[22px] w-[22px] shrink-0 items-center justify-center rounded-full bg-[#FFCC02]/10'>
<GithubIcon width={11} height={11} className='text-[#FFCC02]/75' />
</div>
<div className='flex flex-col gap-[3px]'>
<strong className='font-[430] font-season text-small text-white leading-none'>
Open Source
</strong>
<span className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_55%,transparent)] text-xs leading-none tracking-[0.02em] transition-colors group-hover:text-[color-mix(in_srgb,var(--landing-text-subtle)_75%,transparent)]'>
View on GitHub
</span>
</div>
</Link>
{/* SSO */}
<div className='flex items-center gap-3 px-4 py-3.5'>
<div className='flex h-[22px] w-[22px] shrink-0 items-center justify-center rounded-full bg-[#2ABBF8]/10'>
<Lock className='h-[14px] w-[14px] text-[#2ABBF8]/75' />
</div>
<div className='flex flex-col gap-[3px]'>
<strong className='font-[430] font-season text-small text-white leading-none'>
SSO & SCIM
</strong>
<span className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_55%,transparent)] text-xs leading-none tracking-[0.02em]'>
Okta, Azure AD, Google
</span>
</div>
</div>
</div>
)
}
export default function Enterprise() {
return (
<section
id='enterprise'
aria-labelledby='enterprise-heading'
className='bg-[var(--landing-bg-section)]'
>
<div className='px-4 pt-[60px] pb-10 sm:px-8 sm:pt-20 sm:pb-0 md:px-16 md:pt-[100px]'>
<div className='flex flex-col items-start gap-3 sm:gap-4 md:gap-5'>
<Badge
variant='blue'
size='md'
dot
className='bg-[#FFCC02]/10 font-season text-[#FFCC02] uppercase tracking-[0.02em]'
>
Enterprise
</Badge>
<h2
id='enterprise-heading'
className='max-w-[600px] text-balance font-[430] font-season text-[32px] text-[var(--landing-text-dark)] leading-[100%] tracking-[-0.02em] sm:text-[36px] md:text-[40px]'
>
Enterprise features for
<br />
fast, scalable workflows
</h2>
</div>
<div className='mt-8 overflow-hidden rounded-[12px] bg-[var(--landing-bg)] sm:mt-10 md:mt-12'>
<div className='grid grid-cols-1 border-[var(--landing-border)] border-b lg:grid-cols-[1fr_420px]'>
{/* Audit Trail */}
<div className='border-[var(--landing-border)] lg:border-r'>
<div className='px-6 pt-6 md:px-8 md:pt-8'>
<h3 className='font-[430] font-season text-[16px] text-white leading-[120%] tracking-[-0.01em]'>
Audit Trail
</h3>
<p className='mt-2 max-w-[480px] font-[430] font-season text-[#F6F6F6]/70 text-[14px] leading-[150%] tracking-[0.02em]'>
Every action is captured with full actor attribution.
</p>
</div>
<AuditLogPreview />
<div className='h-6 md:h-8' />
</div>
{/* Access Control */}
<div className='border-[var(--landing-border)] border-t lg:border-t-0'>
<div className='px-6 pt-6 md:px-8 md:pt-8'>
<h3 className='font-[430] font-season text-[16px] text-white leading-[120%] tracking-[-0.01em]'>
Access Control
</h3>
<p className='mt-1.5 font-[430] font-season text-[#F6F6F6]/70 text-[14px] leading-[150%] tracking-[0.02em]'>
Restrict providers, surfaces, and tools per group.
</p>
</div>
<div className='mt-5 px-6 pb-6 md:mt-6 md:px-8 md:pb-8'>
<AccessControlPanel />
</div>
</div>
</div>
<TrustStrip />
{/* Scrolling feature ticker — keyframe loop; pause on hover. Tags use transitions for hover. */}
<div className='enterprise-feature-marquee relative mt-6 overflow-hidden border-[var(--landing-bg-elevated)] border-t'>
<style dangerouslySetInnerHTML={{ __html: ENTERPRISE_FEATURE_MARQUEE_STYLES }} />
{/* Fade edges */}
<div
aria-hidden='true'
className='pointer-events-none absolute top-0 bottom-0 left-0 z-10 w-24'
style={{ background: 'linear-gradient(to right, var(--landing-bg), transparent)' }}
/>
<div
aria-hidden='true'
className='pointer-events-none absolute top-0 right-0 bottom-0 z-10 w-24'
style={{ background: 'linear-gradient(to left, var(--landing-bg), transparent)' }}
/>
{/* Duplicate tags for seamless loop */}
<div className='enterprise-feature-marquee-track flex w-max'>
{[...FEATURE_TAGS, ...FEATURE_TAGS, ...FEATURE_TAGS, ...FEATURE_TAGS].map(
(tag, i) => (
<span
key={i}
className='enterprise-feature-marquee-tag whitespace-nowrap border-[var(--landing-bg-elevated)] border-r px-5 py-4 font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-small leading-none tracking-[0.02em] hover:bg-white/[0.04] hover:text-[color-mix(in_srgb,var(--landing-text-subtle)_80%,transparent)]'
>
{tag}
</span>
)
)}
</div>
</div>
<div className='flex items-center justify-between border-[var(--landing-bg-elevated)] border-t px-6 py-5 md:px-8 md:py-6'>
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-base leading-[150%] tracking-[0.02em]'>
Ready for growth?
</p>
<DemoRequestModal>
<button
type='button'
className='group/cta inline-flex h-[32px] cursor-pointer items-center gap-1.5 rounded-[5px] border border-white bg-white px-2.5 font-[430] font-season text-[14px] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
>
Book a demo
<svg
className='h-[10px] w-[10px] shrink-0'
viewBox='0 0 10 10'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<line
x1='0'
y1='5'
x2='9'
y2='5'
stroke='currentColor'
strokeWidth='1.33'
strokeLinecap='square'
className='origin-left scale-x-0 transition-transform duration-200 ease-out [transform-box:fill-box] group-hover/cta:scale-x-100'
/>
<path
d='M3.5 2L6.5 5L3.5 8'
stroke='currentColor'
strokeWidth='1.33'
strokeLinecap='square'
strokeLinejoin='miter'
fill='none'
className='transition-transform duration-200 ease-out group-hover/cta:translate-x-[30%]'
/>
</svg>
</button>
</DemoRequestModal>
</div>
</div>
</div>
</section>
)
}
@@ -1,7 +1,7 @@
'use client'
import { type SVGProps, useEffect, useRef, useState } from 'react'
import { AnimatePresence, motion, useInView } from 'framer-motion'
import { AnimatePresence, domAnimation, LazyMotion, m, useInView } from 'framer-motion'
import { Streamdown } from 'streamdown'
import 'streamdown/styles.css'
import { ChevronDown } from '@/components/emcn'
@@ -28,30 +28,32 @@ export function FeaturesPreview({ activeTab }: FeaturesPreviewProps) {
const isWorkspaceTab = activeTab <= 3
return (
<div className='relative h-[350px] w-full md:h-[560px]'>
<motion.div
className='absolute inset-0'
animate={{ opacity: isWorkspaceTab ? 1 : 0 }}
transition={{ duration: 0.15 }}
style={{ pointerEvents: isWorkspaceTab ? 'auto' : 'none' }}
>
<WorkspacePreview activeTab={activeTab} isActive={isWorkspaceTab} />
</motion.div>
<AnimatePresence>
{!isWorkspaceTab && (
<motion.div
key={activeTab}
className='absolute inset-0'
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
>
<DefaultPreview />
</motion.div>
)}
</AnimatePresence>
</div>
<LazyMotion features={domAnimation}>
<div className='relative h-[350px] w-full md:h-[560px]'>
<m.div
className='absolute inset-0'
animate={{ opacity: isWorkspaceTab ? 1 : 0 }}
transition={{ duration: 0.15 }}
style={{ pointerEvents: isWorkspaceTab ? 'auto' : 'none' }}
>
<WorkspacePreview activeTab={activeTab} isActive={isWorkspaceTab} />
</m.div>
<AnimatePresence>
{!isWorkspaceTab && (
<m.div
key={activeTab}
className='absolute inset-0'
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
>
<DefaultPreview />
</m.div>
)}
</AnimatePresence>
</div>
</LazyMotion>
)
}
@@ -212,7 +214,7 @@ function WorkspacePreview({ activeTab, isActive }: { activeTab: number; isActive
return (
<div ref={containerRef} className='relative h-[350px] w-full overflow-hidden md:h-[560px]'>
<motion.div
<m.div
aria-hidden='true'
className='absolute inset-0'
animate={{ opacity: isExpanded ? 0 : 1 }}
@@ -228,7 +230,7 @@ function WorkspacePreview({ activeTab, isActive }: { activeTab: number; isActive
<AnimatePresence>
{isMothership && !showGrid && inView && (
<motion.div
<m.div
key='mock-input'
className='absolute inset-0 z-10 flex items-center justify-center'
initial={{ opacity: 0, y: 12 }}
@@ -237,7 +239,7 @@ function WorkspacePreview({ activeTab, isActive }: { activeTab: number; isActive
transition={{ duration: 0.45, ease: [0.4, 0, 0.2, 1] }}
>
<MockUserInput text={typedText} />
</motion.div>
</m.div>
)}
</AnimatePresence>
@@ -254,7 +256,7 @@ function WorkspacePreview({ activeTab, isActive }: { activeTab: number; isActive
}}
>
{MOTHERSHIP_CARDS.map((card) => (
<motion.div
<m.div
key={`${card.row}-${card.col}`}
className='absolute'
initial={gridAnimateIn.current ? { opacity: 0, scale: 0.7, y: 6 } : false}
@@ -272,13 +274,13 @@ function WorkspacePreview({ activeTab, isActive }: { activeTab: number; isActive
}}
>
<MiniCard variant={card.variant} label={card.label} color={card.color} />
</motion.div>
</m.div>
))}
</div>
)}
{isExpanded && expandTarget && (
<motion.div
<m.div
key={expandedTab}
className='absolute inset-0 overflow-hidden border border-[#E5E5E5] bg-white'
initial={{ opacity: 0, scale: 0.15 }}
@@ -291,7 +293,7 @@ function WorkspacePreview({ activeTab, isActive }: { activeTab: number; isActive
{expandedTab === 1 && <MockFullTable revealedRows={revealedRows} />}
{expandedTab === 2 && <MockFullFiles />}
{expandedTab === 3 && <MockFullLogs revealedRows={revealedRows} />}
</motion.div>
</m.div>
)}
</div>
)
@@ -302,20 +304,20 @@ function WorkspacePreview({ activeTab, isActive }: { activeTab: number; isActive
function MockUserInput({ text }: { text: string }) {
return (
<div className='flex w-[380px] items-center gap-1.5 rounded-[16px] border border-[#E0E0E0] bg-white px-2.5 py-2 shadow-[0_2px_8px_rgba(0,0,0,0.06)]'>
<div className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-full border border-[#E8E8E8]'>
<div className='flex size-[24px] flex-shrink-0 items-center justify-center rounded-full border border-[#E8E8E8]'>
<svg width='12' height='12' viewBox='0 0 12 12' fill='none'>
<path d='M6 2.5v7M2.5 6h7' stroke='#999' strokeWidth='1.5' strokeLinecap='round' />
</svg>
</div>
<div className='min-h-[20px] flex-1 font-[430] text-[#1C1C1C] text-[13px] leading-[20px]'>
{text}
<motion.span
<m.span
className='ml-[1px] inline-block h-[14px] w-[1.5px] bg-[#1C1C1C] align-text-bottom'
animate={{ opacity: [1, 0] }}
transition={{ duration: 0.5, repeat: Number.POSITIVE_INFINITY, repeatType: 'reverse' }}
/>
</div>
<div className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-full bg-[#383838]'>
<div className='flex size-[24px] flex-shrink-0 items-center justify-center rounded-full bg-[#383838]'>
<svg width='12' height='12' viewBox='0 0 12 12' fill='none'>
<path
d='M6 9V3M3.5 5L6 2.5L8.5 5'
@@ -381,7 +383,7 @@ function MiniCardIcon({ variant, color }: { variant: CardVariant; color?: string
const c = color ?? '#7C3AED'
return (
<div
className='h-[7px] w-[7px] flex-shrink-0 rounded-[1.5px] border'
className='size-[7px] flex-shrink-0 rounded-[1.5px] border'
style={{
backgroundColor: c,
borderColor: workflowBorderColor(c),
@@ -465,10 +467,10 @@ function TableCardBody() {
function WorkflowCardBody({ color }: { color: string }) {
return (
<div className='relative h-full w-full'>
<div className='absolute top-2.5 left-[10px] h-[14px] w-[14px] rounded-[3px] border border-[#E0E0E0] bg-[#F8F8F8]' />
<div className='absolute top-2.5 left-[10px] size-[14px] rounded-[3px] border border-[#E0E0E0] bg-[#F8F8F8]' />
<div className='absolute top-[16px] left-[24px] h-[1px] w-[16px] bg-[#D8D8D8]' />
<div
className='absolute top-2.5 left-[40px] h-[14px] w-[14px] rounded-[3px] border-[2px]'
className='absolute top-2.5 left-[40px] size-[14px] rounded-[3px] border-[2px]'
style={{
backgroundColor: color,
borderColor: workflowBorderColor(color),
@@ -476,10 +478,10 @@ function WorkflowCardBody({ color }: { color: string }) {
}}
/>
<div className='absolute top-6 left-[46px] h-[12px] w-[1px] bg-[#D8D8D8]' />
<div className='absolute top-[36px] left-[40px] h-[14px] w-[14px] rounded-[3px] border border-[#E0E0E0] bg-[#F8F8F8]' />
<div className='absolute top-[36px] left-[40px] size-[14px] rounded-[3px] border border-[#E0E0E0] bg-[#F8F8F8]' />
<div className='absolute top-[42px] left-[54px] h-[1px] w-[14px] bg-[#D8D8D8]' />
<div
className='absolute top-[36px] left-[68px] h-[14px] w-[14px] rounded-[3px] border-[2px]'
className='absolute top-[36px] left-[68px] size-[14px] rounded-[3px] border-[2px]'
style={{
backgroundColor: color,
borderColor: workflowBorderColor(color),
@@ -506,7 +508,7 @@ function LogsCardBody() {
{LOG_ENTRIES.map((entry, i) => (
<div key={i} className='flex items-center gap-1 py-[1px]'>
<div
className='h-[3px] w-[3px] flex-shrink-0 rounded-full'
className='size-[3px] flex-shrink-0 rounded-full'
style={{ backgroundColor: entry.color }}
/>
<div
@@ -592,7 +594,7 @@ function MockFullFiles() {
<div className='flex h-full flex-col'>
<div className='flex h-[44px] shrink-0 items-center border-[#E5E5E5] border-b px-6'>
<div className='flex items-center gap-1.5'>
<File className='h-[14px] w-[14px] text-[#999]' />
<File className='size-[14px] text-[#999]' />
<span className='text-[#999] text-[13px]'>Files</span>
<span className='text-[#D4D4D4] text-[13px]'>/</span>
<span className='font-medium text-[#1C1C1C] text-[13px]'>meeting-notes.md</span>
@@ -600,7 +602,7 @@ function MockFullFiles() {
</div>
<div className='flex flex-1 overflow-hidden'>
<motion.div
<m.div
className='h-full w-1/2 shrink-0 overflow-hidden'
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@@ -613,11 +615,11 @@ function MockFullFiles() {
autoCorrect='off'
className='h-full w-full resize-none overflow-auto whitespace-pre-wrap bg-transparent p-6 font-[300] font-mono text-[#1C1C1C] text-[12px] leading-[1.7] outline-none'
/>
</motion.div>
</m.div>
<div className='h-full w-px shrink-0 bg-[#E5E5E5]' />
<motion.div
<m.div
className='h-full min-w-0 flex-1 overflow-hidden'
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@@ -628,7 +630,7 @@ function MockFullFiles() {
{source}
</Streamdown>
</div>
</motion.div>
</m.div>
</div>
</div>
)
@@ -744,7 +746,7 @@ function MockFullLogs({ revealedRows }: { revealedRows: number }) {
<div className='flex min-w-0 flex-1 flex-col'>
<div className='flex h-[44px] shrink-0 items-center border-[#E5E5E5] border-b px-6'>
<div className='flex items-center gap-1.5'>
<Library className='h-[14px] w-[14px] text-[#999]' />
<Library className='size-[14px] text-[#999]' />
<span className='font-medium text-[#1C1C1C] text-[13px]'>Logs</span>
</div>
</div>
@@ -770,7 +772,7 @@ function MockFullLogs({ revealedRows }: { revealedRows: number }) {
const statusStyle = LOG_STATUS_STYLES[row[2]] ?? LOG_STATUS_STYLES.success
const isSelected = showSidebar && i === selectedRow
return (
<motion.tr
<m.tr
key={i}
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
@@ -784,7 +786,7 @@ function MockFullLogs({ revealedRows }: { revealedRows: number }) {
<td className='px-6 py-2.5 align-middle'>
<span className='flex items-center gap-3 font-medium text-[#1C1C1C] text-[14px]'>
<div
className='h-[10px] w-[10px] shrink-0 rounded-[3px] border-[1.5px]'
className='size-[10px] shrink-0 rounded-[3px] border-[1.5px]'
style={{
backgroundColor: MOCK_LOG_COLORS[i],
borderColor: `${MOCK_LOG_COLORS[i]}60`,
@@ -816,7 +818,7 @@ function MockFullLogs({ revealedRows }: { revealedRows: number }) {
<td className='px-6 py-2.5 align-middle'>
<span className='font-medium text-[#999] text-[14px]'>{row[5]}</span>
</td>
</motion.tr>
</m.tr>
)
})}
</tbody>
@@ -824,7 +826,7 @@ function MockFullLogs({ revealedRows }: { revealedRows: number }) {
</div>
</div>
<motion.div
<m.div
className='absolute top-0 right-0 bottom-0 z-10 border-[#E5E5E5] border-l bg-white'
initial={{ x: '100%' }}
animate={{ x: showSidebar ? 0 : '100%' }}
@@ -836,7 +838,7 @@ function MockFullLogs({ revealedRows }: { revealedRows: number }) {
onPrev={() => setSelectedRow((r) => Math.max(0, r - 1))}
onNext={() => setSelectedRow((r) => Math.min(MOCK_LOG_DATA.length - 1, r + 1))}
/>
</motion.div>
</m.div>
</div>
)
}
@@ -871,7 +873,7 @@ function MockLogDetailsSidebar({ selectedRow, onPrev, onNext }: MockLogDetailsSi
isPrevDisabled ? 'cursor-not-allowed opacity-40' : 'hover:bg-[#F5F5F5]'
)}
>
<ChevronDown className='h-[14px] w-[14px] rotate-180' />
<ChevronDown className='size-[14px] rotate-180' />
</button>
<button
type='button'
@@ -882,7 +884,7 @@ function MockLogDetailsSidebar({ selectedRow, onPrev, onNext }: MockLogDetailsSi
isNextDisabled ? 'cursor-not-allowed opacity-40' : 'hover:bg-[#F5F5F5]'
)}
>
<ChevronDown className='h-[14px] w-[14px]' />
<ChevronDown className='size-[14px]' />
</button>
</div>
</div>
@@ -900,7 +902,7 @@ function MockLogDetailsSidebar({ selectedRow, onPrev, onNext }: MockLogDetailsSi
<span className='font-medium text-[#999] text-[12px]'>Workflow</span>
<div className='flex items-center gap-2'>
<div
className='h-[10px] w-[10px] shrink-0 rounded-[3px] border-[1.5px]'
className='size-[10px] shrink-0 rounded-[3px] border-[1.5px]'
style={{
backgroundColor: color,
borderColor: workflowBorderColor(color),
@@ -972,7 +974,7 @@ function MockFullTable({ revealedRows }: { revealedRows: number }) {
<div className='flex h-full flex-col'>
<div className='flex h-[44px] shrink-0 items-center border-[#E5E5E5] border-b px-6'>
<div className='flex items-center gap-1.5'>
<Table className='h-[14px] w-[14px] text-[#999]' />
<Table className='size-[14px] text-[#999]' />
<span className='text-[#999] text-[13px]'>Tables</span>
<span className='text-[#D4D4D4] text-[13px]'>/</span>
<span className='font-medium text-[#1C1C1C] text-[13px]'>Leads</span>
@@ -1002,7 +1004,7 @@ function MockFullTable({ revealedRows }: { revealedRows: number }) {
<tr>
<th className='border-[#E5E5E5] border-r border-b bg-[#FAFAFA] px-1 py-[7px] text-center align-middle'>
<div className='flex items-center justify-center'>
<div className='h-[13px] w-[13px] rounded-[2px] border border-[#D4D4D4]' />
<div className='size-[13px] rounded-[2px] border border-[#D4D4D4]' />
</div>
</th>
{MOCK_TABLE_COLUMNS.map((col) => (
@@ -1023,7 +1025,7 @@ function MockFullTable({ revealedRows }: { revealedRows: number }) {
{MOCK_TABLE_DATA.slice(0, revealedRows).map((row, i) => {
const isSelected = selectedRow === i
return (
<motion.tr
<m.tr
key={i}
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
@@ -1059,7 +1061,7 @@ function MockFullTable({ revealedRows }: { revealedRows: number }) {
<span className='block truncate text-[#1C1C1C] text-[13px]'>{cell}</span>
</td>
))}
</motion.tr>
</m.tr>
)
})}
</tbody>
@@ -1072,7 +1074,7 @@ function MockFullTable({ revealedRows }: { revealedRows: number }) {
function ColumnTypeIcon() {
return (
<svg
className='h-3 w-3 shrink-0 text-[#999]'
className='size-3 shrink-0 text-[#999]'
viewBox='0 0 16 16'
fill='none'
stroke='currentColor'
@@ -1163,7 +1165,7 @@ function DefaultPreview() {
const explodeDelay = EXPLODE_BASE_DELAY + index * EXPLODE_STAGGER
return (
<motion.div
<m.div
key={key}
className='absolute flex items-center justify-center rounded-xl border border-[#E5E5E5] bg-white p-2.5 shadow-[0_2px_4px_0_rgba(0,0,0,0.06)]'
initial={{ top: '50%', left: '50%', opacity: 0, scale: 0, x: '-50%', y: '-50%' }}
@@ -1177,19 +1179,19 @@ function DefaultPreview() {
style={{ color }}
aria-label={label}
>
<Icon className='h-6 w-6' />
</motion.div>
<Icon className='size-6' />
</m.div>
)
})}
<motion.div
<m.div
className='absolute top-1/2 left-[48%]'
initial={{ opacity: 0, x: '-50%', y: '-50%' }}
animate={inView ? { opacity: 1, x: '-50%', y: '-50%' } : undefined}
transition={{ duration: 0.4, ease: 'easeOut', delay: 0 }}
>
<div className='flex h-[36px] items-center gap-2 rounded-[8px] border border-[#E5E5E5] bg-white px-2.5 shadow-[0_2px_6px_0_rgba(0,0,0,0.08)]'>
<div className='flex h-[22px] w-[22px] flex-shrink-0 items-center justify-center rounded-[5px] bg-[#1e1e1e]'>
<div className='flex size-[22px] flex-shrink-0 items-center justify-center rounded-[5px] bg-[#1e1e1e]'>
<svg width='11' height='11' viewBox='0 0 10 10' fill='none'>
<path
d='M1 9C1 4.58 4.58 1 9 1'
@@ -1204,7 +1206,7 @@ function DefaultPreview() {
</span>
<ChevronDown className='h-[8px] w-[10px] flex-shrink-0 text-[#999]' />
</div>
</motion.div>
</m.div>
</div>
)
}
@@ -1,7 +1,14 @@
'use client'
import { useRef, useState } from 'react'
import { type MotionValue, motion, useScroll, useTransform } from 'framer-motion'
import {
domAnimation,
LazyMotion,
type MotionValue,
m,
useScroll,
useTransform,
} from 'framer-motion'
import dynamic from 'next/dynamic'
import Image from 'next/image'
import { Badge } from '@/components/emcn'
@@ -132,7 +139,7 @@ function ScrollLetter({ scrollYProgress, charIndex, children }: ScrollLetterProp
const threshold = (charIndex / HEADING_LETTERS.length) * LETTER_REVEAL_SPAN
const opacity = useTransform(scrollYProgress, [threshold, threshold + LETTER_FADE_IN], [0.4, 1])
return <motion.span style={{ opacity }}>{children}</motion.span>
return <m.span style={{ opacity }}>{children}</m.span>
}
export default function Features() {
@@ -145,154 +152,159 @@ export default function Features() {
})
return (
<section
id='features'
aria-labelledby='features-heading'
className='relative overflow-hidden bg-[var(--landing-bg-section)]'
>
<div aria-hidden='true' className='absolute top-0 left-0 hidden w-full lg:block'>
<Image
src='/landing/features-transition.svg'
alt=''
width={1440}
height={366}
className='h-auto w-full'
/>
</div>
<div className='relative z-10 pt-[60px] lg:pt-[100px]'>
<div ref={sectionRef} className='flex flex-col items-start gap-5 px-6 lg:px-16'>
<Badge
variant='blue'
size='md'
dot
className='font-season uppercase tracking-[0.02em] transition-colors duration-200'
style={{
color: FEATURE_TABS[activeTab].badgeColor ?? FEATURE_TABS[activeTab].color,
backgroundColor: hexToRgba(
FEATURE_TABS[activeTab].badgeColor ?? FEATURE_TABS[activeTab].color,
0.1
),
}}
>
Workspace
</Badge>
<p className='sr-only'>
Sim's workspace includes four core features: Mothership, an AI command center for
natural-language control of your entire workspace; Tables, a built-in database for
filtering, sorting, and wiring data directly into workflows; Files, a shared document
store for uploading, creating, and sharing documents, spreadsheets, and media across
teams and agents; and Logs, full execution tracing with inputs, outputs, cost, and
duration for every run.
</p>
<h2
id='features-heading'
className='max-w-[900px] text-balance font-[430] font-season text-[24px] text-[var(--landing-text-dark)] leading-[110%] tracking-[-0.02em] md:text-[36px]'
>
{HEADING_LETTERS.map((char, i) => (
<ScrollLetter key={i} scrollYProgress={scrollYProgress} charIndex={i}>
{char}
</ScrollLetter>
))}
<span className='text-[color-mix(in_srgb,var(--landing-text-dark)_40%,transparent)]'>
Build agents, connect your data, and monitor every run — all in one workspace.
</span>
</h2>
<LazyMotion features={domAnimation}>
<section
id='features'
aria-labelledby='features-heading'
className='relative overflow-hidden bg-[var(--landing-bg-section)]'
>
<div aria-hidden='true' className='absolute top-0 left-0 hidden w-full lg:block'>
<Image
src='/landing/features-transition.svg'
alt=''
width={1440}
height={366}
className='h-auto w-full'
/>
</div>
<div className='relative mt-10 pb-[60px] lg:mt-[73px] lg:pb-[100px]'>
<div
aria-hidden='true'
className='absolute top-0 bottom-0 left-16 z-20 hidden w-px bg-[var(--divider)] lg:block'
/>
<div
aria-hidden='true'
className='absolute top-0 right-16 bottom-0 z-20 hidden w-px bg-[var(--divider)] lg:block'
/>
<div className='flex h-[68px] border border-[var(--divider)] lg:overflow-hidden'>
<div
aria-hidden='true'
className='h-full w-[24px] shrink-0 bg-[var(--landing-bg-section)] lg:w-16'
/>
<div role='tablist' aria-label='Feature categories' className='flex flex-1'>
{FEATURE_TABS.map((tab, index) => (
<button
key={tab.label}
id={`feature-tab-${index}`}
type='button'
role='tab'
aria-selected={index === activeTab}
aria-controls='features-panel'
onClick={() => setActiveTab(index)}
className={`relative h-full min-w-0 flex-1 items-center justify-center px-2 font-medium font-season text-[var(--landing-text-dark)] text-caption uppercase lg:px-0 lg:text-sm${tab.hideOnMobile ? ' hidden lg:flex' : ' flex'}${index > 0 ? ' border-[var(--divider)] border-l' : ''}`}
style={{ backgroundColor: index === activeTab ? '#FDFDFD' : '#F6F6F6' }}
>
<span className='truncate'>{tab.label}</span>
{index === activeTab && (
<div className='absolute right-0 bottom-0 left-0 flex h-[6px]'>
{tab.segments.map(([opacity, width], i) => (
<div
key={i}
className='h-full shrink-0'
style={{
width: `${width}%`,
backgroundColor: tab.color,
opacity,
}}
/>
))}
</div>
)}
</button>
<div className='relative z-10 pt-[60px] lg:pt-[100px]'>
<div ref={sectionRef} className='flex flex-col items-start gap-5 px-6 lg:px-16'>
<Badge
variant='blue'
size='md'
dot
className='font-season uppercase tracking-[0.02em] transition-colors duration-200'
style={{
color: FEATURE_TABS[activeTab].badgeColor ?? FEATURE_TABS[activeTab].color,
backgroundColor: hexToRgba(
FEATURE_TABS[activeTab].badgeColor ?? FEATURE_TABS[activeTab].color,
0.1
),
}}
>
Workspace
</Badge>
<p className='sr-only'>
Sim's workspace includes four core features: Mothership, an AI command center for
natural-language control of your entire workspace; Tables, a built-in database for
filtering, sorting, and wiring data directly into workflows; Files, a shared document
store for uploading, creating, and sharing documents, spreadsheets, and media across
teams and agents; and Logs, full execution tracing with inputs, outputs, cost, and
duration for every run.
</p>
<h2
id='features-heading'
className='max-w-[900px] text-balance font-[430] font-season text-[24px] text-[var(--landing-text-dark)] leading-[110%] tracking-[-0.02em] md:text-[36px]'
>
{HEADING_LETTERS.map((char, i) => (
<ScrollLetter key={i} scrollYProgress={scrollYProgress} charIndex={i}>
{char}
</ScrollLetter>
))}
<span className='text-[color-mix(in_srgb,var(--landing-text-dark)_40%,transparent)]'>
Build agents, connect your data, and monitor every run, all in one workspace.
</span>
</h2>
</div>
<div className='relative mt-10 pb-[60px] lg:mt-[73px] lg:pb-[100px]'>
<div
aria-hidden='true'
className='absolute top-0 bottom-0 left-16 z-20 hidden w-px bg-[var(--divider)] lg:block'
/>
<div
aria-hidden='true'
className='absolute top-0 right-16 bottom-0 z-20 hidden w-px bg-[var(--divider)] lg:block'
/>
<div className='flex h-[68px] border border-[var(--divider)] lg:overflow-hidden'>
<div
aria-hidden='true'
className='h-full w-[24px] shrink-0 bg-[var(--landing-bg-section)] lg:w-16'
/>
<div role='tablist' aria-label='Feature categories' className='flex flex-1'>
{FEATURE_TABS.map((tab, index) => (
<button
key={tab.label}
id={`feature-tab-${index}`}
type='button'
role='tab'
aria-selected={index === activeTab}
aria-controls='features-panel'
onClick={() => setActiveTab(index)}
className={`relative h-full min-w-0 flex-1 items-center justify-center px-2 font-medium font-season text-[var(--landing-text-dark)] text-caption uppercase lg:px-0 lg:text-sm${tab.hideOnMobile ? ' hidden lg:flex' : ' flex'}${index > 0 ? ' border-[var(--divider)] border-l' : ''}`}
style={{ backgroundColor: index === activeTab ? '#FDFDFD' : '#F6F6F6' }}
>
<span className='truncate'>{tab.label}</span>
{index === activeTab && (
<div className='absolute right-0 bottom-0 left-0 flex h-[6px]'>
{tab.segments.map(([opacity, width], i) => (
<div
key={i}
className='h-full shrink-0'
style={{
width: `${width}%`,
backgroundColor: tab.color,
opacity,
}}
/>
))}
</div>
)}
</button>
))}
</div>
<div
aria-hidden='true'
className='h-full w-[24px] shrink-0 border-[var(--divider)] border-l bg-[var(--landing-bg-section)] lg:w-16'
/>
</div>
<div
id='features-panel'
role='tabpanel'
aria-labelledby={`feature-tab-${activeTab}`}
className='mt-8 flex flex-col gap-6 px-6 lg:mt-[60px] lg:grid lg:grid-cols-[1fr_2.8fr] lg:gap-[60px] lg:px-[104px]'
>
<div className='flex flex-col items-start justify-between gap-6 pt-5 lg:h-[560px] lg:gap-0'>
<div className='flex flex-col items-start gap-4'>
<h3 className='font-[430] font-season text-[24px] text-[var(--landing-text-dark)] leading-[120%] tracking-[-0.02em] lg:text-[28px]'>
{FEATURE_TABS[activeTab].title}
</h3>
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-dark)_50%,transparent)] text-md leading-[150%] tracking-[0.02em] lg:text-lg'>
{FEATURE_TABS[activeTab].description}
</p>
</div>
<AuthModal defaultView='signup' source='features'>
<button
type='button'
className='inline-flex h-[32px] items-center rounded-[5px] border border-[#1D1D1D] bg-[#1D1D1D] px-2.5 font-[430] font-season text-sm text-white transition-colors hover:border-[var(--landing-bg-elevated)] hover:bg-[var(--landing-bg-elevated)]'
onClick={() =>
trackLandingCta({
label: FEATURE_TABS[activeTab].cta,
section: 'features',
destination: 'auth_modal',
})
}
>
{FEATURE_TABS[activeTab].cta}
</button>
</AuthModal>
</div>
<FeaturesPreview activeTab={activeTab} />
</div>
<div
aria-hidden='true'
className='h-full w-[24px] shrink-0 border-[var(--divider)] border-l bg-[var(--landing-bg-section)] lg:w-16'
className='mt-[60px] hidden h-px bg-[var(--divider)] lg:block'
/>
</div>
<div
id='features-panel'
role='tabpanel'
aria-labelledby={`feature-tab-${activeTab}`}
className='mt-8 flex flex-col gap-6 px-6 lg:mt-[60px] lg:grid lg:grid-cols-[1fr_2.8fr] lg:gap-[60px] lg:px-[104px]'
>
<div className='flex flex-col items-start justify-between gap-6 pt-5 lg:h-[560px] lg:gap-0'>
<div className='flex flex-col items-start gap-4'>
<h3 className='font-[430] font-season text-[24px] text-[var(--landing-text-dark)] leading-[120%] tracking-[-0.02em] lg:text-[28px]'>
{FEATURE_TABS[activeTab].title}
</h3>
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-dark)_50%,transparent)] text-md leading-[150%] tracking-[0.02em] lg:text-lg'>
{FEATURE_TABS[activeTab].description}
</p>
</div>
<AuthModal defaultView='signup' source='features'>
<button
type='button'
className='inline-flex h-[32px] items-center rounded-[5px] border border-[#1D1D1D] bg-[#1D1D1D] px-2.5 font-[430] font-season text-sm text-white transition-colors hover:border-[var(--landing-bg-elevated)] hover:bg-[var(--landing-bg-elevated)]'
onClick={() =>
trackLandingCta({
label: FEATURE_TABS[activeTab].cta,
section: 'features',
destination: 'auth_modal',
})
}
>
{FEATURE_TABS[activeTab].cta}
</button>
</AuthModal>
</div>
<FeaturesPreview activeTab={activeTab} />
</div>
<div aria-hidden='true' className='mt-[60px] hidden h-px bg-[var(--divider)] lg:block' />
</div>
</div>
</section>
</section>
</LazyMotion>
)
}
@@ -4,6 +4,7 @@ import { useCallback, useRef, useState } from 'react'
import { ArrowUp } from 'lucide-react'
import dynamic from 'next/dynamic'
import { cn } from '@/lib/core/utils/cn'
import { handleKeyboardActivation } from '@/lib/core/utils/keyboard'
import { captureClientEvent } from '@/lib/posthog/client'
import { useLandingSubmit } from '@/app/(landing)/components/landing-preview/components/landing-preview-panel/landing-preview-panel'
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
@@ -64,8 +65,14 @@ export function FooterCTA() {
<div className='mt-8 w-full max-w-[42rem]'>
<div
role='group'
aria-label='Landing prompt input'
className='cursor-text rounded-[20px] border border-[var(--landing-bg-elevated)] bg-[var(--landing-bg-surface)] px-2.5 py-2'
onClick={() => textareaRef.current?.focus()}
onKeyDown={(event) => {
if (event.target !== event.currentTarget) return
handleKeyboardActivation(event, () => textareaRef.current?.focus())
}}
>
<textarea
ref={textareaRef}
@@ -76,7 +83,7 @@ export function FooterCTA() {
aria-label='Describe what you want to build'
placeholder={animatedPlaceholder}
rows={2}
className='m-0 box-border min-h-[48px] w-full resize-none border-0 bg-transparent px-1 py-1 font-body text-[var(--landing-text)] text-base leading-[24px] tracking-[-0.015em] caret-white outline-none placeholder:font-[380] placeholder:text-[var(--landing-text-muted)] focus-visible:ring-0'
className='m-0 box-border min-h-[48px] w-full resize-none border-0 bg-transparent p-1 font-body text-[var(--landing-text)] text-base leading-[24px] tracking-[-0.015em] caret-white outline-none placeholder:font-[380] placeholder:text-[var(--landing-text-muted)] focus-visible:ring-0'
style={{ maxHeight: `${MAX_HEIGHT}px` }}
/>
<div className='flex items-center justify-end'>
@@ -42,8 +42,8 @@ export default function Hero() {
>
<p className='sr-only'>
Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect
1,000+ integrations and every major LLM including OpenAI, Anthropic Claude, Google Gemini,
Mistral, and xAI Grok to create agents that automate real work. Build agents visually with
1,000+ integrations and every major LLM, including OpenAI, Anthropic Claude, Google Gemini,
Mistral, and xAI Grok, to create agents that automate real work. Build agents visually with
the workflow builder, conversationally through Mothership, or programmatically with the API.
Trusted by over 100,000 builders at startups and Fortune 500 companies. SOC2 compliant.
</p>
@@ -1,5 +1,4 @@
import Collaboration from '@/app/(landing)/components/collaboration/collaboration'
import Enterprise from '@/app/(landing)/components/enterprise/enterprise'
import ExternalRedirect from '@/app/(landing)/components/external-redirect'
import Features from '@/app/(landing)/components/features/features'
import Footer from '@/app/(landing)/components/footer/footer'
@@ -13,7 +12,6 @@ import Testimonials from '@/app/(landing)/components/testimonials/testimonials'
export {
Collaboration,
Enterprise,
ExternalRedirect,
Features,
Footer,
@@ -1,11 +1,11 @@
'use client'
import { useState } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { AnimatePresence, domAnimation, LazyMotion, m } from 'framer-motion'
import { ChevronDown } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
export interface LandingFAQItem {
interface LandingFAQItem {
question: string
answer: string
}
@@ -19,67 +19,69 @@ export function LandingFAQ({ faqs }: LandingFAQProps) {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null)
return (
<div>
{faqs.map(({ question, answer }, index) => {
const isOpen = openIndex === index
const isHovered = hoveredIndex === index
const showDivider = index > 0 && hoveredIndex !== index && hoveredIndex !== index - 1
<LazyMotion features={domAnimation}>
<div>
{faqs.map(({ question, answer }, index) => {
const isOpen = openIndex === index
const isHovered = hoveredIndex === index
const showDivider = index > 0 && hoveredIndex !== index && hoveredIndex !== index - 1
return (
<div key={question}>
<div
className={cn(
'h-px w-full bg-[var(--landing-bg-elevated)]',
index === 0 || !showDivider ? 'invisible' : 'visible'
)}
/>
<button
type='button'
onClick={() => setOpenIndex(isOpen ? null : index)}
onMouseEnter={() => setHoveredIndex(index)}
onMouseLeave={() => setHoveredIndex(null)}
className='-mx-6 flex w-[calc(100%+3rem)] items-center justify-between gap-4 px-6 py-4 text-left transition-colors hover:bg-[var(--landing-bg-elevated)]'
aria-expanded={isOpen}
>
<span
return (
<div key={question}>
<div
className={cn(
'text-[15px] leading-snug tracking-[-0.02em] transition-colors',
isOpen
? 'text-[var(--landing-text)]'
: 'text-[var(--landing-text-body)] hover:text-[var(--landing-text)]'
'h-px w-full bg-[var(--landing-bg-elevated)]',
index === 0 || !showDivider ? 'invisible' : 'visible'
)}
>
{question}
</span>
<ChevronDown
className={cn(
'h-3 w-3 shrink-0 text-[var(--landing-text-subtle)] transition-transform duration-200',
isOpen ? 'rotate-180' : 'rotate-0'
)}
aria-hidden='true'
/>
</button>
<AnimatePresence initial={false}>
{isOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.25, ease: [0.4, 0, 0.2, 1] }}
className='overflow-hidden'
<button
type='button'
onClick={() => setOpenIndex(isOpen ? null : index)}
onMouseEnter={() => setHoveredIndex(index)}
onMouseLeave={() => setHoveredIndex(null)}
className='-mx-6 flex w-[calc(100%+3rem)] items-center justify-between gap-4 px-6 py-4 text-left transition-colors hover:bg-[var(--landing-bg-elevated)]'
aria-expanded={isOpen}
>
<span
className={cn(
'text-[15px] leading-snug tracking-[-0.02em] transition-colors',
isOpen
? 'text-[var(--landing-text)]'
: 'text-[var(--landing-text-body)] hover:text-[var(--landing-text)]'
)}
>
<div className='pt-2 pb-4'>
<p className='text-[14px] text-[var(--landing-text-body)] leading-[1.75]'>
{answer}
</p>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
)
})}
</div>
{question}
</span>
<ChevronDown
className={cn(
'h-3 w-3 shrink-0 text-[var(--landing-text-subtle)] transition-transform duration-200',
isOpen ? 'rotate-180' : 'rotate-0'
)}
aria-hidden='true'
/>
</button>
<AnimatePresence initial={false}>
{isOpen && (
<m.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.25, ease: [0.4, 0, 0.2, 1] }}
className='overflow-hidden'
>
<div className='pt-2 pb-4'>
<p className='text-[14px] text-[var(--landing-text-body)] leading-[1.75]'>
{answer}
</p>
</div>
</m.div>
)}
</AnimatePresence>
</div>
)
})}
</div>
</LazyMotion>
)
}
@@ -81,9 +81,9 @@ const ROWS: PreviewRow[] = [
{
id: '1',
cells: {
name: { icon: <PdfIcon className='h-[14px] w-[14px]' />, label: 'Q1 Performance Report.pdf' },
name: { icon: <PdfIcon className='size-[14px]' />, label: 'Q1 Performance Report.pdf' },
size: { label: '2.4 MB' },
type: { icon: <PdfIcon className='h-[14px] w-[14px]' />, label: 'PDF' },
type: { icon: <PdfIcon className='size-[14px]' />, label: 'PDF' },
created: { label: '3 hours ago' },
owner: ownerCell('T', 'Theo L.'),
},
@@ -91,9 +91,9 @@ const ROWS: PreviewRow[] = [
{
id: '2',
cells: {
name: { icon: <ZipIcon className='h-[14px] w-[14px]' />, label: 'product-screenshots.zip' },
name: { icon: <ZipIcon className='size-[14px]' />, label: 'product-screenshots.zip' },
size: { label: '18.7 MB' },
type: { icon: <ZipIcon className='h-[14px] w-[14px]' />, label: 'ZIP' },
type: { icon: <ZipIcon className='size-[14px]' />, label: 'ZIP' },
created: { label: '1 day ago' },
owner: ownerCell('A', 'Alex M.'),
},
@@ -101,9 +101,9 @@ const ROWS: PreviewRow[] = [
{
id: '3',
cells: {
name: { icon: <JsonlIcon className='h-[14px] w-[14px]' />, label: 'training-dataset.jsonl' },
name: { icon: <JsonlIcon className='size-[14px]' />, label: 'training-dataset.jsonl' },
size: { label: '892 KB' },
type: { icon: <JsonlIcon className='h-[14px] w-[14px]' />, label: 'JSONL' },
type: { icon: <JsonlIcon className='size-[14px]' />, label: 'JSONL' },
created: { label: '3 days ago' },
owner: ownerCell('J', 'Jordan P.'),
},
@@ -111,9 +111,9 @@ const ROWS: PreviewRow[] = [
{
id: '4',
cells: {
name: { icon: <PdfIcon className='h-[14px] w-[14px]' />, label: 'brand-guidelines.pdf' },
name: { icon: <PdfIcon className='size-[14px]' />, label: 'brand-guidelines.pdf' },
size: { label: '5.1 MB' },
type: { icon: <PdfIcon className='h-[14px] w-[14px]' />, label: 'PDF' },
type: { icon: <PdfIcon className='size-[14px]' />, label: 'PDF' },
created: { label: '1 week ago' },
owner: ownerCell('S', 'Sarah K.'),
},
@@ -121,9 +121,9 @@ const ROWS: PreviewRow[] = [
{
id: '5',
cells: {
name: { icon: <AudioIcon className='h-[14px] w-[14px]' />, label: 'customer-interviews.mp3' },
name: { icon: <AudioIcon className='size-[14px]' />, label: 'customer-interviews.mp3' },
size: { label: '45.2 MB' },
type: { icon: <AudioIcon className='h-[14px] w-[14px]' />, label: 'Audio' },
type: { icon: <AudioIcon className='size-[14px]' />, label: 'Audio' },
created: { label: 'March 20th, 2026' },
owner: ownerCell('V', 'Vik M.'),
},
@@ -131,9 +131,9 @@ const ROWS: PreviewRow[] = [
{
id: '6',
cells: {
name: { icon: <DocxIcon className='h-[14px] w-[14px]' />, label: 'onboarding-playbook.docx' },
name: { icon: <DocxIcon className='size-[14px]' />, label: 'onboarding-playbook.docx' },
size: { label: '1.1 MB' },
type: { icon: <DocxIcon className='h-[14px] w-[14px]' />, label: 'DOCX' },
type: { icon: <DocxIcon className='size-[14px]' />, label: 'DOCX' },
created: { label: 'March 14th, 2026' },
owner: ownerCell('S', 'Sarah K.'),
},
@@ -1,10 +1,11 @@
'use client'
import { memo, useCallback, useEffect, useRef, useState } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { AnimatePresence, domAnimation, LazyMotion, m } from 'framer-motion'
import { ArrowUp, Table } from 'lucide-react'
import { Blimp, Checkbox, ChevronDown } from '@/components/emcn'
import { TypeBoolean, TypeNumber, TypeText } from '@/components/emcn/icons'
import { handleKeyboardActivation } from '@/lib/core/utils/keyboard'
import { captureClientEvent } from '@/lib/posthog/client'
import { useLandingSubmit } from '@/app/(landing)/components/landing-preview/components/landing-preview-panel/landing-preview-panel'
import { EASE_OUT } from '@/app/(landing)/components/landing-preview/components/landing-preview-workflow/workflow-data'
@@ -177,176 +178,183 @@ export const LandingPreviewHome = memo(function LandingPreviewHome({
const showToolCall = chatPhase === 'tool-call' || isResponding
return (
<div className='flex min-w-0 flex-1 overflow-hidden'>
{/* Chat area — matches mothership-view layout */}
<div className='min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-6 pt-4 pb-8'>
<div className='mx-auto max-w-[42rem] space-y-6'>
{/* User message — rounded bubble, right-aligned */}
<motion.div
className='flex flex-col items-end gap-[6px] pt-3'
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, ease: EASE_OUT }}
>
<div className='max-w-[70%] overflow-hidden rounded-[16px] bg-[#363636] px-3.5 py-2'>
<p
className='font-body text-[14px] leading-[1.5]'
style={{ color: C.TEXT_PRIMARY }}
>
{AUTO_PROMPT}
</p>
</div>
</motion.div>
{/* Assistant — no bubble, full-width prose */}
<AnimatePresence>
{showToolCall && (
<motion.div
className='space-y-2.5'
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, ease: EASE_OUT }}
>
{/* Agent group header — icon + label + chevron */}
<button
type='button'
onClick={() => setToolsExpanded((p) => !p)}
className='flex cursor-pointer items-center gap-2'
<LazyMotion features={domAnimation}>
<div className='flex min-w-0 flex-1 overflow-hidden'>
{/* Chat area — matches mothership-view layout */}
<div className='min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-6 pt-4 pb-8'>
<div className='mx-auto max-w-[42rem] space-y-6'>
{/* User message — rounded bubble, right-aligned */}
<m.div
className='flex flex-col items-end gap-[6px] pt-3'
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, ease: EASE_OUT }}
>
<div className='max-w-[70%] overflow-hidden rounded-[16px] bg-[#363636] px-3.5 py-2'>
<p
className='font-body text-[14px] leading-[1.5]'
style={{ color: C.TEXT_PRIMARY }}
>
<div className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center'>
<Blimp className='h-[16px] w-[16px]' style={{ color: C.TEXT_ICON }} />
</div>
<span className='font-base text-sm' style={{ color: C.TEXT_BODY }}>
Mothership
</span>
<ChevronDown
className='h-[7px] w-[9px] transition-transform duration-150'
{AUTO_PROMPT}
</p>
</div>
</m.div>
{/* Assistant — no bubble, full-width prose */}
<AnimatePresence>
{showToolCall && (
<m.div
className='space-y-2.5'
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, ease: EASE_OUT }}
>
{/* Agent group header — icon + label + chevron */}
<button
type='button'
onClick={() => setToolsExpanded((p) => !p)}
className='flex cursor-pointer items-center gap-2'
>
<div className='flex size-[16px] flex-shrink-0 items-center justify-center'>
<Blimp className='size-[16px]' style={{ color: C.TEXT_ICON }} />
</div>
<span className='font-base text-sm' style={{ color: C.TEXT_BODY }}>
Mothership
</span>
<ChevronDown
className='h-[7px] w-[9px] transition-transform duration-150'
style={{
color: C.TEXT_ICON,
transform: toolsExpanded ? 'rotate(0deg)' : 'rotate(-90deg)',
}}
/>
</button>
{/* Tool call items — collapsible */}
<div
className='grid transition-[grid-template-rows] duration-200 ease-out'
style={{
color: C.TEXT_ICON,
transform: toolsExpanded ? 'rotate(0deg)' : 'rotate(-90deg)',
gridTemplateRows: toolsExpanded ? '1fr' : '0fr',
}}
/>
</button>
{/* Tool call items — collapsible */}
<div
className='grid transition-[grid-template-rows] duration-200 ease-out'
style={{
gridTemplateRows: toolsExpanded ? '1fr' : '0fr',
}}
>
<div className='overflow-hidden'>
<div className='flex flex-col gap-1.5 pt-0.5'>
<ToolCallRow
icon={
<Table
className='h-[15px] w-[15px]'
style={{ color: C.TEXT_TERTIARY }}
/>
}
title='Read Customer Leads'
/>
>
<div className='overflow-hidden'>
<div className='flex flex-col gap-1.5 pt-0.5'>
<ToolCallRow
icon={
<Table className='size-[15px]' style={{ color: C.TEXT_TERTIARY }} />
}
title='Read Customer Leads'
/>
</div>
</div>
</div>
</div>
{/* Response prose — full width, no card */}
{isResponding && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2, ease: EASE_OUT }}
>
<ChatMarkdown
content={MOCK_RESPONSE}
visibleLength={responseTypedLength}
isTyping={chatPhase === 'responding'}
/>
</motion.div>
)}
</motion.div>
)}
</AnimatePresence>
{/* Response prose — full width, no card */}
{isResponding && (
<m.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2, ease: EASE_OUT }}
>
<ChatMarkdown
content={MOCK_RESPONSE}
visibleLength={responseTypedLength}
isTyping={chatPhase === 'responding'}
/>
</m.div>
)}
</m.div>
)}
</AnimatePresence>
</div>
</div>
</div>
{/* Resource panel — slides in from right */}
<AnimatePresence>
{showResourcePanel && (
<motion.div
className='hidden h-full flex-shrink-0 overflow-hidden border-[#2c2c2c] border-l lg:flex'
initial={{ width: 0, opacity: 0 }}
animate={{ width: '55%', opacity: 1 }}
transition={{ duration: 0.35, ease: EASE_OUT }}
>
<MiniTablePanel />
</motion.div>
)}
</AnimatePresence>
</div>
{/* Resource panel — slides in from right */}
<AnimatePresence>
{showResourcePanel && (
<m.div
className='hidden h-full flex-shrink-0 overflow-hidden border-[#2c2c2c] border-l lg:flex'
initial={{ width: 0, opacity: 0 }}
animate={{ width: '55%', opacity: 1 }}
transition={{ duration: 0.35, ease: EASE_OUT }}
>
<MiniTablePanel />
</m.div>
)}
</AnimatePresence>
</div>
</LazyMotion>
)
}
return (
<div className='flex min-w-0 flex-1 flex-col items-center justify-center px-6 pb-[2vh]'>
<motion.p
role='presentation'
className='mb-6 max-w-[42rem] font-[430] font-season text-[32px] tracking-[-0.02em]'
style={{ color: C.TEXT_PRIMARY }}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, ease: EASE_OUT }}
>
What should we get done?
</motion.p>
<motion.div
className='w-full max-w-[32rem]'
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.1, ease: EASE_OUT }}
>
<div
className='cursor-text rounded-[20px] border px-2.5 py-2'
style={{ borderColor: C.BORDER, backgroundColor: C.SURFACE }}
onClick={() => textareaRef.current?.focus()}
<LazyMotion features={domAnimation}>
<div className='flex min-w-0 flex-1 flex-col items-center justify-center px-6 pb-[2vh]'>
<m.p
role='presentation'
className='mb-6 max-w-[42rem] font-[430] font-season text-[32px] tracking-[-0.02em]'
style={{ color: C.TEXT_PRIMARY }}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, ease: EASE_OUT }}
>
<textarea
ref={textareaRef}
value={inputValue}
onChange={(e) => {
if (!autoType) setInputValue(e.target.value)
What should we get done?
</m.p>
<m.div
className='w-full max-w-[32rem]'
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.1, ease: EASE_OUT }}
>
<div
role='group'
aria-label='Preview message input'
className='cursor-text rounded-[20px] border px-2.5 py-2'
style={{ borderColor: C.BORDER, backgroundColor: C.SURFACE }}
onClick={() => textareaRef.current?.focus()}
onKeyDown={(event) => {
if (event.target !== event.currentTarget) return
handleKeyboardActivation(event, () => textareaRef.current?.focus())
}}
onKeyDown={handleKeyDown}
onInput={handleInput}
placeholder={animatedPlaceholder}
rows={1}
readOnly={autoType}
className='m-0 box-border min-h-[24px] w-full resize-none overflow-y-auto border-0 bg-transparent px-1 py-1 font-body text-[15px] leading-[24px] tracking-[-0.015em] outline-none placeholder:font-[380] placeholder:text-[#787878] focus-visible:ring-0'
style={{
color: C.TEXT_PRIMARY,
caretColor: autoType ? 'transparent' : C.TEXT_PRIMARY,
maxHeight: '200px',
}}
/>
<div className='flex items-center justify-end'>
<button
type='button'
onClick={handleSubmit}
disabled={isEmpty}
aria-label='Submit message'
className='flex h-[28px] w-[28px] items-center justify-center rounded-full border-0 p-0 transition-colors'
style={{
background: isEmpty ? '#808080' : '#e0e0e0',
cursor: isEmpty ? 'not-allowed' : 'pointer',
>
<textarea
ref={textareaRef}
value={inputValue}
onChange={(e) => {
if (!autoType) setInputValue(e.target.value)
}}
>
<ArrowUp size={16} strokeWidth={2.25} color='#1b1b1b' />
</button>
onKeyDown={handleKeyDown}
onInput={handleInput}
placeholder={animatedPlaceholder}
rows={1}
readOnly={autoType}
className='m-0 box-border min-h-[24px] w-full resize-none overflow-y-auto border-0 bg-transparent p-1 font-body text-[15px] leading-[24px] tracking-[-0.015em] outline-none placeholder:font-[380] placeholder:text-[#787878] focus-visible:ring-0'
style={{
color: C.TEXT_PRIMARY,
caretColor: autoType ? 'transparent' : C.TEXT_PRIMARY,
maxHeight: '200px',
}}
/>
<div className='flex items-center justify-end'>
<button
type='button'
onClick={handleSubmit}
disabled={isEmpty}
aria-label='Submit message'
className='flex size-[28px] items-center justify-center rounded-full border-0 p-0 transition-colors'
style={{
background: isEmpty ? '#808080' : '#e0e0e0',
cursor: isEmpty ? 'not-allowed' : 'pointer',
}}
>
<ArrowUp size={16} strokeWidth={2.25} color='#1b1b1b' />
</button>
</div>
</div>
</div>
</motion.div>
</div>
</m.div>
</div>
</LazyMotion>
)
})
@@ -357,7 +365,7 @@ export const LandingPreviewHome = memo(function LandingPreviewHome({
function ToolCallRow({ icon, title }: { icon: React.ReactNode; title: string }) {
return (
<div className='flex items-center gap-[8px] pl-[24px]'>
<div className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center'>{icon}</div>
<div className='flex size-[16px] flex-shrink-0 items-center justify-center'>{icon}</div>
<span className='font-base text-[13px]' style={{ color: C.TEXT_SECONDARY }}>
{title}
</span>
@@ -385,7 +393,7 @@ function ChatMarkdown({
<div className='font-body text-[14px] leading-[1.6]' style={{ color: C.TEXT_PRIMARY }}>
<span dangerouslySetInnerHTML={{ __html: rendered }} />
{isTyping && (
<motion.span
<m.span
className='inline-block h-[14px] w-[1.5px] translate-y-[2px] bg-[#e6e6e6]'
animate={{ opacity: [1, 0] }}
transition={{
@@ -406,7 +414,7 @@ function MiniTablePanel() {
return (
<div className='flex h-full w-full flex-col bg-[var(--landing-bg)]'>
<div className='flex items-center gap-2 border-[#2c2c2c] border-b px-3 py-2'>
<Table className='h-[14px] w-[14px]' style={{ color: C.TEXT_ICON }} />
<Table className='size-[14px]' style={{ color: C.TEXT_ICON }} />
<span className='font-medium text-sm' style={{ color: C.TEXT_PRIMARY }}>
Customer Leads
</span>
@@ -428,7 +436,7 @@ function MiniTablePanel() {
className='border-[#2c2c2c] border-r border-b bg-[#1e1e1e] p-0 text-left'
>
<div className='flex items-center gap-1 px-2 py-1.5'>
<Icon className='h-3 w-3 shrink-0' style={{ color: C.TEXT_ICON }} />
<Icon className='size-3 shrink-0' style={{ color: C.TEXT_ICON }} />
<span className='font-medium text-[11px]' style={{ color: C.TEXT_PRIMARY }}>
{col.label}
</span>
@@ -444,7 +452,7 @@ function MiniTablePanel() {
</thead>
<tbody>
{MINI_TABLE_ROWS.map((row, i) => (
<motion.tr
<m.tr
key={i}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@@ -472,7 +480,7 @@ function MiniTablePanel() {
</td>
)
})}
</motion.tr>
</m.tr>
))}
</tbody>
</table>
@@ -16,14 +16,14 @@ import type {
} from '@/app/(landing)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
import { LandingPreviewResource } from '@/app/(landing)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
const DB_ICON = <Database className='h-[14px] w-[14px]' />
const DB_ICON = <Database className='size-[14px]' />
function connectorIcons(icons: React.ComponentType<{ className?: string }>[]) {
return {
content: (
<div className='flex items-center gap-1'>
{icons.map((Icon, i) => (
<Icon key={i} className='h-3.5 w-3.5 flex-shrink-0' />
<Icon key={i} className='size-3.5 flex-shrink-0' />
))}
</div>
),
@@ -174,12 +174,12 @@ export function LandingPreviewLogs() {
<div className='border-[var(--border)] border-b px-6 py-2.5'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-3'>
<Library className='h-[14px] w-[14px] text-[var(--text-icon)]' />
<Library className='size-[14px] text-[var(--text-icon)]' />
<h1 className='font-medium text-[var(--text-body)] text-sm'>Logs</h1>
</div>
<div className='flex items-center gap-1'>
<div className='flex cursor-default items-center rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption'>
<Download className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
<Download className='mr-1.5 size-[14px] text-[var(--text-icon)]' />
Export
</div>
<button
@@ -213,7 +213,7 @@ export function LandingPreviewLogs() {
<div className='border-[var(--border)] border-b px-6 py-2.5'>
<div className='flex items-center justify-between'>
<div className='flex flex-1 items-center gap-2.5'>
<Search className='h-[14px] w-[14px] flex-shrink-0 text-[var(--text-icon)]' />
<Search className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
<input
type='text'
value={search}
@@ -224,7 +224,7 @@ export function LandingPreviewLogs() {
</div>
<div className='flex items-center gap-1.5'>
<div className='flex cursor-default items-center rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption'>
<ListFilter className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
<ListFilter className='mr-1.5 size-[14px] text-[var(--text-icon)]' />
Filter
</div>
<button
@@ -232,7 +232,7 @@ export function LandingPreviewLogs() {
onClick={() => handleSort(sortKey ?? 'workflowName')}
className='flex cursor-default items-center rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption transition-colors hover-hover:bg-[var(--surface-3)]'
>
<ArrowUpDown className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
<ArrowUpDown className='mr-1.5 size-[14px] text-[var(--text-icon)]' />
Sort
</button>
</div>
@@ -266,7 +266,7 @@ export function LandingPreviewLogs() {
)}
>
{label}
{sortKey === key && <ArrowUpDown className='h-[10px] w-[10px] opacity-60' />}
{sortKey === key && <ArrowUpDown className='size-[10px] opacity-60' />}
</button>
</th>
))}
@@ -281,7 +281,7 @@ export function LandingPreviewLogs() {
<td className='px-6 align-middle'>
<div className='flex items-center gap-2'>
<div
className='h-[10px] w-[10px] flex-shrink-0 rounded-[3px] border-[1.5px]'
className='size-[10px] flex-shrink-0 rounded-[3px] border-[1.5px]'
style={{
backgroundColor: log.workflowColor,
borderColor: workflowBorderColor(log.workflowColor),
@@ -1,7 +1,7 @@
'use client'
import { memo, useCallback, useEffect, useRef, useState } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { AnimatePresence, domMax, LazyMotion, m } from 'framer-motion'
import { ArrowUp } from 'lucide-react'
import dynamic from 'next/dynamic'
import { useRouter } from 'next/navigation'
@@ -174,166 +174,168 @@ export const LandingPreviewPanel = memo(function LandingPreviewPanel({
)
return (
<div className='flex h-full w-[280px] flex-shrink-0 flex-col bg-[#1e1e1e]'>
<div className='flex h-full flex-col border-[#2c2c2c] border-l pt-3.5'>
{/* Header */}
<div className='flex flex-shrink-0 items-center justify-between px-2'>
<div className='pointer-events-none flex gap-1.5'>
<div className='flex h-[30px] w-[30px] items-center justify-center rounded-[5px] border border-[#3d3d3d] bg-[#363636]'>
<MoreHorizontal className='h-[14px] w-[14px] text-[#e6e6e6]' />
</div>
<div className='flex h-[30px] w-[30px] items-center justify-center rounded-[5px] border border-[#3d3d3d] bg-[#363636]'>
<BubbleChatPreview className='h-[14px] w-[14px] text-[#e6e6e6]' />
</div>
</div>
<AuthModal defaultView='signup' source='landing_preview'>
<button
type='button'
className='flex gap-1.5'
onMouseMove={(e) => setCursorPos({ x: e.clientX, y: e.clientY })}
onMouseLeave={() => setCursorPos(null)}
onClick={() =>
trackLandingCta({
label: 'Deploy',
section: 'landing_preview',
destination: 'auth_modal',
})
}
>
<div className='flex h-[30px] items-center rounded-[5px] bg-[#33C482] px-2.5 transition-colors hover:bg-[#2DAC72]'>
<span className='font-medium text-[#1b1b1b] text-[12px]'>Deploy</span>
<LazyMotion features={domMax}>
<div className='flex h-full w-[280px] flex-shrink-0 flex-col bg-[#1e1e1e]'>
<div className='flex h-full flex-col border-[#2c2c2c] border-l pt-3.5'>
{/* Header */}
<div className='flex flex-shrink-0 items-center justify-between px-2'>
<div className='pointer-events-none flex gap-1.5'>
<div className='flex size-[30px] items-center justify-center rounded-[5px] border border-[#3d3d3d] bg-[#363636]'>
<MoreHorizontal className='size-[14px] text-[#e6e6e6]' />
</div>
<div className='flex h-[30px] items-center gap-2 rounded-[5px] bg-[#33C482] px-2.5 transition-colors hover:bg-[#2DAC72]'>
<Play className='h-[11.5px] w-[11.5px] text-[#1b1b1b]' />
<span className='font-medium text-[#1b1b1b] text-[12px]'>Run</span>
<div className='flex size-[30px] items-center justify-center rounded-[5px] border border-[#3d3d3d] bg-[#363636]'>
<BubbleChatPreview className='size-[14px] text-[#e6e6e6]' />
</div>
</button>
</AuthModal>
{cursorPos &&
createPortal(
<div
className='pointer-events-none fixed z-[9999]'
style={{ left: cursorPos.x + 14, top: cursorPos.y + 14 }}
</div>
<AuthModal defaultView='signup' source='landing_preview'>
<button
type='button'
className='flex gap-1.5'
onMouseMove={(e) => setCursorPos({ x: e.clientX, y: e.clientY })}
onMouseLeave={() => setCursorPos(null)}
onClick={() =>
trackLandingCta({
label: 'Deploy',
section: 'landing_preview',
destination: 'auth_modal',
})
}
>
<div className='flex h-[4px]'>
<div className='h-full w-[8px] bg-[#2ABBF8]' />
<div className='h-full w-[14px] bg-[#2ABBF8] opacity-60' />
<div className='h-full w-[8px] bg-[#00F701]' />
<div className='h-full w-[16px] bg-[#00F701] opacity-60' />
<div className='h-full w-[8px] bg-[#FFCC02]' />
<div className='h-full w-[10px] bg-[#FFCC02] opacity-60' />
<div className='h-full w-[8px] bg-[#FA4EDF]' />
<div className='h-full w-[14px] bg-[#FA4EDF] opacity-60' />
<div className='flex h-[30px] items-center rounded-[5px] bg-[#33C482] px-2.5 transition-colors hover:bg-[#2DAC72]'>
<span className='font-medium text-[#1b1b1b] text-[12px]'>Deploy</span>
</div>
<div className='flex items-center gap-[5px] bg-white px-1.5 py-1 font-medium text-[#1C1C1C] text-[11px]'>
Get started
<ChevronDown className='-rotate-90 h-[7px] w-[7px] text-[#1C1C1C]' />
<div className='flex h-[30px] items-center gap-2 rounded-[5px] bg-[#33C482] px-2.5 transition-colors hover:bg-[#2DAC72]'>
<Play className='size-[11.5px] text-[#1b1b1b]' />
<span className='font-medium text-[#1b1b1b] text-[12px]'>Run</span>
</div>
</div>,
document.body
)}
</div>
{/* Tabs with sliding active indicator */}
<div className='flex flex-shrink-0 items-center px-2 pt-3.5'>
<div className='flex gap-1'>
{TABS_WITH_TOOLBAR.map((tab) => {
if (tab.disabled) {
return (
<div
key={tab.id}
className='pointer-events-none flex h-[28px] items-center rounded-md border border-transparent px-2 py-[5px]'
>
<span className='font-medium text-[#787878] text-[12.5px]'>{tab.label}</span>
</div>
)
}
const isActive = activeTab === tab.id
return (
<button
key={tab.id}
type='button'
onClick={() => handleTabSwitch(tab.id as PanelTab)}
className='relative flex h-[28px] items-center rounded-md border border-transparent px-2 py-[5px] font-medium text-[12.5px] transition-colors hover:border-[#3d3d3d] hover:bg-[#363636] hover:text-[#e6e6e6]'
style={{ color: isActive ? '#e6e6e6' : '#787878' }}
</button>
</AuthModal>
{cursorPos &&
createPortal(
<div
className='pointer-events-none fixed z-[9999]'
style={{ left: cursorPos.x + 14, top: cursorPos.y + 14 }}
>
{isActive && (
<motion.div
layoutId='panel-tab-indicator'
className='absolute inset-0 rounded-md border border-[#3d3d3d] bg-[#363636]'
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
/>
)}
<span className='relative z-10'>{tab.label}</span>
</button>
)
})}
<div className='flex h-[4px]'>
<div className='h-full w-[8px] bg-[#2ABBF8]' />
<div className='h-full w-[14px] bg-[#2ABBF8] opacity-60' />
<div className='h-full w-[8px] bg-[#00F701]' />
<div className='h-full w-[16px] bg-[#00F701] opacity-60' />
<div className='h-full w-[8px] bg-[#FFCC02]' />
<div className='h-full w-[10px] bg-[#FFCC02] opacity-60' />
<div className='h-full w-[8px] bg-[#FA4EDF]' />
<div className='h-full w-[14px] bg-[#FA4EDF] opacity-60' />
</div>
<div className='flex items-center gap-[5px] bg-white px-1.5 py-1 font-medium text-[#1C1C1C] text-[11px]'>
Get started
<ChevronDown className='-rotate-90 size-[7px] text-[#1C1C1C]' />
</div>
</div>,
document.body
)}
</div>
</div>
{/* Tab content with cross-fade */}
<div className='flex flex-1 flex-col overflow-hidden pt-3'>
<AnimatePresence mode='wait'>
{activeTab === 'copilot' && (
<motion.div
key='copilot'
className='flex h-full flex-col'
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15, ease: EASE_OUT }}
>
<div className='pointer-events-none mx-[-1px] flex flex-shrink-0 items-center justify-between gap-2 border border-[#2c2c2c] bg-[#292929] px-3 py-1.5'>
<span className='min-w-0 flex-1 truncate font-medium text-[#e6e6e6] text-[14px]'>
New Chat
</span>
</div>
<div className='px-2 pt-3 pb-2'>
<div className='rounded-[4px] border border-[#3d3d3d] bg-[#292929] px-1.5 py-1.5'>
<textarea
ref={textareaRef}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder='Build an AI agent...'
rows={2}
className='mb-1.5 min-h-[48px] w-full cursor-text resize-none border-0 bg-transparent px-0.5 py-1 font-base text-[#e6e6e6] text-sm leading-[1.25rem] placeholder-[#787878] caret-[#e6e6e6] outline-none'
/>
<div className='flex items-center justify-end'>
<button
type='button'
onClick={handleSubmit}
disabled={isEmpty}
className='flex h-[22px] w-[22px] items-center justify-center rounded-full border-0 p-0 transition-colors'
style={{
background: isEmpty ? '#808080' : '#e0e0e0',
cursor: isEmpty ? 'not-allowed' : 'pointer',
}}
>
<ArrowUp size={14} strokeWidth={2.25} color='#1b1b1b' />
</button>
{/* Tabs with sliding active indicator */}
<div className='flex flex-shrink-0 items-center px-2 pt-3.5'>
<div className='flex gap-1'>
{TABS_WITH_TOOLBAR.map((tab) => {
if (tab.disabled) {
return (
<div
key={tab.id}
className='pointer-events-none flex h-[28px] items-center rounded-md border border-transparent px-2 py-[5px]'
>
<span className='font-medium text-[#787878] text-[12.5px]'>{tab.label}</span>
</div>
)
}
const isActive = activeTab === tab.id
return (
<button
key={tab.id}
type='button'
onClick={() => handleTabSwitch(tab.id as PanelTab)}
className='relative flex h-[28px] items-center rounded-md border border-transparent px-2 py-[5px] font-medium text-[12.5px] transition-colors hover:border-[#3d3d3d] hover:bg-[#363636] hover:text-[#e6e6e6]'
style={{ color: isActive ? '#e6e6e6' : '#787878' }}
>
{isActive && (
<m.div
layoutId='panel-tab-indicator'
className='absolute inset-0 rounded-md border border-[#3d3d3d] bg-[#363636]'
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
/>
)}
<span className='relative z-10'>{tab.label}</span>
</button>
)
})}
</div>
</div>
{/* Tab content with cross-fade */}
<div className='flex flex-1 flex-col overflow-hidden pt-3'>
<AnimatePresence mode='wait'>
{activeTab === 'copilot' && (
<m.div
key='copilot'
className='flex h-full flex-col'
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15, ease: EASE_OUT }}
>
<div className='pointer-events-none mx-[-1px] flex flex-shrink-0 items-center justify-between gap-2 border border-[#2c2c2c] bg-[#292929] px-3 py-1.5'>
<span className='min-w-0 flex-1 truncate font-medium text-[#e6e6e6] text-[14px]'>
New Chat
</span>
</div>
<div className='px-2 pt-3 pb-2'>
<div className='rounded-[4px] border border-[#3d3d3d] bg-[#292929] p-1.5'>
<textarea
ref={textareaRef}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder='Build an AI agent...'
rows={2}
className='mb-1.5 min-h-[48px] w-full cursor-text resize-none border-0 bg-transparent px-0.5 py-1 font-base text-[#e6e6e6] text-sm leading-[1.25rem] placeholder-[#787878] caret-[#e6e6e6] outline-none'
/>
<div className='flex items-center justify-end'>
<button
type='button'
onClick={handleSubmit}
disabled={isEmpty}
className='flex size-[22px] items-center justify-center rounded-full border-0 p-0 transition-colors'
style={{
background: isEmpty ? '#808080' : '#e0e0e0',
cursor: isEmpty ? 'not-allowed' : 'pointer',
}}
>
<ArrowUp size={14} strokeWidth={2.25} color='#1b1b1b' />
</button>
</div>
</div>
</div>
</div>
</motion.div>
)}
</m.div>
)}
{activeTab === 'editor' && (
<motion.div
key='editor'
className='flex h-full flex-col'
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15, ease: EASE_OUT }}
>
<EditorTabContent editorPrompt={editorPrompt} typedLength={typedLength} />
</motion.div>
)}
</AnimatePresence>
{activeTab === 'editor' && (
<m.div
key='editor'
className='flex h-full flex-col'
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15, ease: EASE_OUT }}
>
<EditorTabContent editorPrompt={editorPrompt} typedLength={typedLength} />
</m.div>
)}
</AnimatePresence>
</div>
</div>
</div>
</div>
</LazyMotion>
)
})
@@ -384,10 +386,10 @@ function EditorTabContent({ editorPrompt, typedLength }: EditorTabContentProps)
<div className='mx-[-1px] flex flex-shrink-0 items-center gap-2 border border-[#2c2c2c] bg-[#292929] px-3 py-1.5'>
{BlockIcon && (
<div
className='flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center rounded-sm'
className='flex size-[18px] flex-shrink-0 items-center justify-center rounded-sm'
style={{ background: bgColor }}
>
<BlockIcon className='h-[12px] w-[12px] text-white' />
<BlockIcon className='size-[12px] text-white' />
</div>
)}
<span className='min-w-0 flex-1 truncate font-medium text-[#e6e6e6] text-sm'>
@@ -403,11 +405,11 @@ function EditorTabContent({ editorPrompt, typedLength }: EditorTabContentProps)
<div className='flex items-center pl-0.5'>
<span className='font-medium text-[#e6e6e6] text-small'>System Prompt</span>
</div>
<div className='rounded-[4px] border border-[#3d3d3d] bg-[#292929] px-2 py-2'>
<div className='rounded-[4px] border border-[#3d3d3d] bg-[#292929] p-2'>
<p className='min-h-[48px] whitespace-pre-wrap break-words font-medium font-sans text-[#e6e6e6] text-sm leading-[1.5]'>
{visibleText}
{isTyping && (
<motion.span
<m.span
className='inline-block h-[14px] w-[1.5px] translate-y-[2px] bg-[#e6e6e6]'
animate={{ opacity: [1, 0] }}
transition={{
@@ -428,7 +430,7 @@ function EditorTabContent({ editorPrompt, typedLength }: EditorTabContentProps)
<span className='font-medium text-[#e6e6e6] text-small'>Model</span>
</div>
<div className='flex h-[32px] items-center gap-2 rounded-[4px] border border-[#3d3d3d] bg-[#292929] px-2'>
{ModelIcon && <ModelIcon className='h-[14px] w-[14px] text-[#e6e6e6]' />}
{ModelIcon && <ModelIcon className='size-[14px] text-[#e6e6e6]' />}
<span className='flex-1 truncate font-medium text-[#e6e6e6] text-sm'>{model}</span>
<ChevronDown className='h-[7px] w-[9px] text-[#636363]' />
</div>
@@ -451,10 +453,10 @@ function EditorTabContent({ editorPrompt, typedLength }: EditorTabContentProps)
>
{ToolIcon && (
<div
className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center rounded-[4px]'
className='flex size-[16px] flex-shrink-0 items-center justify-center rounded-[4px]'
style={{ background: tool.bgColor }}
>
<ToolIcon className='h-[10px] w-[10px] text-white' />
<ToolIcon className='size-[10px] text-white' />
</div>
)}
<span className='font-normal text-[#e6e6e6] text-[12px]'>{tool.name}</span>
@@ -474,7 +476,7 @@ function EditorTabContent({ editorPrompt, typedLength }: EditorTabContentProps)
<div className='relative h-[6px] rounded-full bg-[#3d3d3d]'>
<div className='h-full w-[70%] rounded-full bg-[#e6e6e6]' />
<div
className='-translate-y-1/2 absolute top-1/2 h-[14px] w-[14px] rounded-full border-[#e6e6e6] border-[2px] bg-[#292929]'
className='-translate-y-1/2 absolute top-1/2 size-[14px] rounded-full border-[#e6e6e6] border-[2px] bg-[#292929]'
style={{ left: 'calc(70% - 7px)' }}
/>
</div>
@@ -485,7 +487,7 @@ function EditorTabContent({ editorPrompt, typedLength }: EditorTabContentProps)
<div className='flex items-center pl-0.5'>
<span className='font-medium text-[#e6e6e6] text-small'>Response Format</span>
</div>
<div className='rounded-[4px] border border-[#3d3d3d] bg-[#292929] px-2 py-2'>
<div className='rounded-[4px] border border-[#3d3d3d] bg-[#292929] p-2'>
<span className='font-mono text-[#787878] text-[12px]'>plain text</span>
</div>
</div>
@@ -35,7 +35,7 @@ interface LandingPreviewResourceProps {
export function ownerCell(initial: string, name: string): PreviewCell {
return {
icon: (
<span className='flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center rounded-full border border-[var(--border)] bg-[var(--surface-3)] font-medium text-[8px] text-[var(--text-secondary)]'>
<span className='flex size-[14px] flex-shrink-0 items-center justify-center rounded-full border border-[var(--border)] bg-[var(--surface-3)] font-medium text-[8px] text-[var(--text-secondary)]'>
{initial}
</span>
),
@@ -88,11 +88,11 @@ export function LandingPreviewResource({
<div className='border-[var(--border)] border-b px-6 py-2.5'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-3'>
<Icon className='h-[14px] w-[14px] text-[var(--text-icon)]' />
<Icon className='size-[14px] text-[var(--text-icon)]' />
<h1 className='font-medium text-[var(--text-body)] text-sm'>{title}</h1>
</div>
<div className='flex cursor-default items-center rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption'>
<Plus className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
<Plus className='mr-1.5 size-[14px] text-[var(--text-icon)]' />
{createLabel}
</div>
</div>
@@ -102,7 +102,7 @@ export function LandingPreviewResource({
<div className='border-[var(--border)] border-b px-6 py-2.5'>
<div className='flex items-center justify-between'>
<div className='flex flex-1 items-center gap-2.5'>
<Search className='h-[14px] w-[14px] flex-shrink-0 text-[var(--text-icon)]' />
<Search className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
<input
type='text'
value={search}
@@ -113,7 +113,7 @@ export function LandingPreviewResource({
</div>
<div className='flex items-center gap-1.5'>
<div className='flex cursor-default items-center rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption'>
<ListFilter className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
<ListFilter className='mr-1.5 size-[14px] text-[var(--text-icon)]' />
Filter
</div>
<button
@@ -121,7 +121,7 @@ export function LandingPreviewResource({
onClick={() => handleSortClick(sortColId ?? columns[0]?.id)}
className='flex cursor-default items-center rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption transition-colors hover-hover:bg-[var(--surface-3)]'
>
<ArrowUpDown className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
<ArrowUpDown className='mr-1.5 size-[14px] text-[var(--text-icon)]' />
Sort
</button>
</div>
@@ -157,9 +157,7 @@ export function LandingPreviewResource({
)}
>
{col.header}
{sortColId === col.id && (
<ArrowUpDown className='h-[10px] w-[10px] opacity-60' />
)}
{sortColId === col.id && <ArrowUpDown className='size-[10px] opacity-60' />}
</button>
</th>
))}
@@ -5,7 +5,7 @@ import type {
} from '@/app/(landing)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
import { LandingPreviewResource } from '@/app/(landing)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
const CAL_ICON = <Calendar className='h-[14px] w-[14px]' />
const CAL_ICON = <Calendar className='size-[14px]' />
const COLUMNS: PreviewColumn[] = [
{ id: 'task', header: 'Task' },
@@ -75,7 +75,7 @@ function NavItem({
if (!onClick) {
return (
<div className='pointer-events-none mx-0.5 flex h-[28px] items-center gap-2 rounded-[8px] px-2'>
<Icon className='h-[14px] w-[14px] flex-shrink-0' style={{ color: C.TEXT_ICON }} />
<Icon className='size-[14px] flex-shrink-0' style={{ color: C.TEXT_ICON }} />
<span className='truncate text-[13px]' style={{ color: C.TEXT_BODY, fontWeight: 450 }}>
{label}
</span>
@@ -92,7 +92,7 @@ function NavItem({
isActive && 'bg-[var(--c-active)]'
)}
>
<Icon className='h-[14px] w-[14px] flex-shrink-0' style={{ color: C.TEXT_ICON }} />
<Icon className='size-[14px] flex-shrink-0' style={{ color: C.TEXT_ICON }} />
<span className='truncate text-[13px]' style={{ color: C.TEXT_BODY, fontWeight: 450 }}>
{label}
</span>
@@ -128,7 +128,7 @@ export function LandingPreviewSidebar({
className='pointer-events-none flex h-[32px] w-full items-center gap-2 rounded-[8px] border pr-2 pl-[5px]'
style={{ borderColor: C.BORDER, backgroundColor: C.SURFACE_2 }}
>
<div className='flex h-[20px] w-[20px] flex-shrink-0 items-center justify-center rounded-[4px] bg-white'>
<div className='flex size-[20px] flex-shrink-0 items-center justify-center rounded-[4px] bg-white'>
<svg width='10' height='10' viewBox='0 0 10 10' fill='none'>
<path
d='M1 9C1 4.58 4.58 1 9 1'
@@ -158,7 +158,7 @@ export function LandingPreviewSidebar({
isHomeActive && 'bg-[var(--c-active)]'
)}
>
<Home className='h-[14px] w-[14px] flex-shrink-0' style={{ color: C.TEXT_ICON }} />
<Home className='size-[14px] flex-shrink-0' style={{ color: C.TEXT_ICON }} />
<span className='truncate text-[13px]' style={{ color: C.TEXT_BODY, fontWeight: 450 }}>
Home
</span>
@@ -209,7 +209,7 @@ export function LandingPreviewSidebar({
)}
>
<div
className='h-[14px] w-[14px] flex-shrink-0 rounded-[4px] border-[2.5px]'
className='size-[14px] flex-shrink-0 rounded-[4px] border-[2.5px]'
style={{
backgroundColor: workflow.color,
borderColor: workflowBorderColor(workflow.color),
@@ -1,7 +1,7 @@
'use client'
import { useEffect, useState } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { AnimatePresence, domAnimation, LazyMotion, m } from 'framer-motion'
import { Checkbox } from '@/components/emcn'
import {
ChevronDown,
@@ -50,9 +50,9 @@ const TABLE_METAS: Record<string, string> = {
'5': 'Invoice Records',
}
const TABLE_ICON = <Table className='h-[14px] w-[14px]' />
const COLUMNS_ICON = <Columns3 className='h-[14px] w-[14px]' />
const ROWS_ICON = <Rows3 className='h-[14px] w-[14px]' />
const TABLE_ICON = <Table className='size-[14px]' />
const COLUMNS_ICON = <Columns3 className='size-[14px]' />
const ROWS_ICON = <Rows3 className='size-[14px]' />
const LIST_ROWS: PreviewRow[] = [
{
@@ -440,7 +440,7 @@ function SpreadsheetView({ tableId, tableName, onBack }: SpreadsheetViewProps) {
onClick={onBack}
className='inline-flex items-center px-2 py-1 font-medium text-[var(--text-secondary)] text-sm transition-colors hover-hover:text-[var(--text-body)]'
>
<Table className='mr-3 h-[14px] w-[14px] text-[var(--text-icon)]' />
<Table className='mr-3 size-[14px] text-[var(--text-icon)]' />
Tables
</button>
<span className='select-none text-[var(--text-icon)] text-sm'>/</span>
@@ -468,7 +468,7 @@ function SpreadsheetView({ tableId, tableName, onBack }: SpreadsheetViewProps) {
return (
<th key={col.id} className={CELL_HEADER}>
<div className='flex h-full w-full min-w-0 items-center px-2 py-[7px]'>
<Icon className='h-3 w-3 shrink-0 text-[var(--text-icon)]' />
<Icon className='size-3 shrink-0 text-[var(--text-icon)]' />
<span className='ml-1.5 min-w-0 overflow-clip text-ellipsis whitespace-nowrap font-medium text-[var(--text-primary)] text-small'>
{col.label}
</span>
@@ -549,36 +549,34 @@ export function LandingPreviewTables({ autoOpenTableId }: LandingPreviewTablesPr
}, [autoOpenTableId])
return (
<AnimatePresence mode='wait'>
{openTableId !== null ? (
<motion.div
key={`spreadsheet-${openTableId}`}
className='flex h-full flex-1 flex-col'
{...tableViewTransition}
>
<SpreadsheetView
tableId={openTableId}
tableName={TABLE_METAS[openTableId] ?? 'Table'}
onBack={() => setOpenTableId(null)}
/>
</motion.div>
) : (
<motion.div
key='table-list'
className='flex h-full flex-1 flex-col'
{...tableViewTransition}
>
<LandingPreviewResource
icon={Table}
title='Tables'
createLabel='New table'
searchPlaceholder='Search tables...'
columns={LIST_COLUMNS}
rows={LIST_ROWS}
onRowClick={(id) => setOpenTableId(id)}
/>
</motion.div>
)}
</AnimatePresence>
<LazyMotion features={domAnimation}>
<AnimatePresence mode='wait'>
{openTableId !== null ? (
<m.div
key={`spreadsheet-${openTableId}`}
className='flex h-full flex-1 flex-col'
{...tableViewTransition}
>
<SpreadsheetView
tableId={openTableId}
tableName={TABLE_METAS[openTableId] ?? 'Table'}
onBack={() => setOpenTableId(null)}
/>
</m.div>
) : (
<m.div key='table-list' className='flex h-full flex-1 flex-col' {...tableViewTransition}>
<LandingPreviewResource
icon={Table}
title='Tables'
createLabel='New table'
searchPlaceholder='Search tables...'
columns={LIST_COLUMNS}
rows={LIST_ROWS}
onRowClick={(id) => setOpenTableId(id)}
/>
</m.div>
)}
</AnimatePresence>
</LazyMotion>
)
}
@@ -1,7 +1,7 @@
'use client'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { motion } from 'framer-motion'
import { domAnimation, LazyMotion, m } from 'framer-motion'
import ReactFlow, {
applyEdgeChanges,
applyNodeChanges,
@@ -62,7 +62,7 @@ function PreviewEdge({
if (data?.animate) {
return (
<motion.path
<m.path
id={id}
className='react-flow__edge-path'
d={edgePath}
@@ -172,15 +172,17 @@ export function LandingPreviewWorkflow({
highlightedBlockId,
}: LandingPreviewWorkflowProps) {
return (
<div className='h-full w-full'>
<ReactFlowProvider key={workflow.id}>
<PreviewFlow
workflow={workflow}
animate={animate}
fitViewOptions={fitViewOptions}
highlightedBlockId={highlightedBlockId}
/>
</ReactFlowProvider>
</div>
<LazyMotion features={domAnimation}>
<div className='h-full w-full'>
<ReactFlowProvider key={workflow.id}>
<PreviewFlow
workflow={workflow}
animate={animate}
fitViewOptions={fitViewOptions}
highlightedBlockId={highlightedBlockId}
/>
</ReactFlowProvider>
</div>
</LazyMotion>
)
}
@@ -1,7 +1,7 @@
'use client'
import { memo } from 'react'
import { motion } from 'framer-motion'
import { domAnimation, LazyMotion, m } from 'framer-motion'
import { Database } from 'lucide-react'
import { Handle, type NodeProps, Position } from 'reactflow'
import { Blimp } from '@/components/emcn'
@@ -145,133 +145,141 @@ export const PreviewBlockNode = memo(function PreviewBlockNode({
if (blockType === 'note' && markdown) {
return (
<motion.div
className='relative'
initial={animate ? { opacity: 0 } : false}
animate={{ opacity: 1 }}
transition={{ duration: 0.45, delay, ease: EASE_OUT }}
>
<div className='w-[280px] select-none rounded-[8px] border border-[#3d3d3d] bg-[#232323]'>
<div className='border-[#3d3d3d] border-b p-2'>
<span className='font-medium text-[#e6e6e6] text-[16px]'>Note</span>
<LazyMotion features={domAnimation}>
<m.div
className='relative'
initial={animate ? { opacity: 0 } : false}
animate={{ opacity: 1 }}
transition={{ duration: 0.45, delay, ease: EASE_OUT }}
>
<div className='w-[280px] select-none rounded-[8px] border border-[#3d3d3d] bg-[#232323]'>
<div className='border-[#3d3d3d] border-b p-2'>
<span className='font-medium text-[#e6e6e6] text-[16px]'>Note</span>
</div>
<div className='p-2.5'>
<NoteMarkdown content={markdown} />
</div>
</div>
<div className='p-2.5'>
<NoteMarkdown content={markdown} />
</div>
</div>
</motion.div>
</m.div>
</LazyMotion>
)
}
const hasContent = rows.length > 0 || (tools && tools.length > 0)
return (
<motion.div
className='relative'
initial={animate ? { opacity: 0 } : false}
animate={{ opacity: 1 }}
transition={{ duration: 0.45, delay, ease: EASE_OUT }}
>
<div className='relative z-[20] w-[250px] select-none rounded-[8px] border border-[#3d3d3d] bg-[#232323]'>
{/* Target handle (left side) */}
{!hideTargetHandle && (
<Handle
type='target'
position={Position.Left}
id='target'
className={HANDLE_LEFT}
style={{ top: '20px', transform: 'translateY(-50%)' }}
isConnectableStart={false}
isConnectableEnd={false}
/>
)}
<LazyMotion features={domAnimation}>
<m.div
className='relative'
initial={animate ? { opacity: 0 } : false}
animate={{ opacity: 1 }}
transition={{ duration: 0.45, delay, ease: EASE_OUT }}
>
<div className='relative z-[20] w-[250px] select-none rounded-[8px] border border-[#3d3d3d] bg-[#232323]'>
{/* Target handle (left side) */}
{!hideTargetHandle && (
<Handle
type='target'
position={Position.Left}
id='target'
className={HANDLE_LEFT}
style={{ top: '20px', transform: 'translateY(-50%)' }}
isConnectableStart={false}
isConnectableEnd={false}
/>
)}
{/* Header */}
<div
className={`flex items-center justify-between p-2 ${hasContent ? 'border-[#3d3d3d] border-b' : ''}`}
>
<div className='relative z-10 flex min-w-0 flex-1 items-center gap-2.5'>
<div
className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-[6px]'
style={{ background: bgColor }}
>
{Icon && <Icon className='h-[16px] w-[16px] text-white' />}
</div>
<span className='truncate font-medium text-[#e6e6e6] text-[16px]'>{name}</span>
</div>
</div>
{/* Sub-block rows + tools */}
{hasContent && (
<div className='flex flex-col gap-2 p-2'>
{rows.map((row) => {
const modelEntry = row.title === 'Model' ? getModelIconEntry(row.value) : null
const ModelIcon = modelEntry?.icon
return (
<div key={row.title} className='flex items-center gap-2'>
<span className='flex-shrink-0 font-normal text-[#b3b3b3] text-[14px] capitalize'>
{row.title}
</span>
{row.value && (
<span className='flex min-w-0 flex-1 items-center justify-end gap-2 font-normal text-[#e6e6e6] text-[14px]'>
{ModelIcon && (
<ModelIcon
className={`inline-block flex-shrink-0 text-[#e6e6e6] ${modelEntry.size ?? 'h-[14px] w-[14px]'}`}
/>
)}
<span className='truncate'>{row.value}</span>
</span>
)}
</div>
)
})}
{/* Tool chips — inline with label */}
{tools && tools.length > 0 && (
<div className='flex items-center gap-2'>
<span className='flex-shrink-0 font-normal text-[#b3b3b3] text-[14px]'>Tools</span>
<div className='flex flex-1 flex-wrap items-center justify-end gap-[5px]'>
{tools.map((tool) => {
const ToolIcon = BLOCK_ICONS[tool.type]
return (
<div
key={tool.type}
className='flex items-center gap-[5px] rounded-[5px] border border-[#3d3d3d] bg-[#2a2a2a] px-[6px] py-[3px]'
>
<div
className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center rounded-[4px]'
style={{ background: tool.bgColor }}
>
{ToolIcon && <ToolIcon className='h-[10px] w-[10px] text-white' />}
</div>
<span className='font-normal text-[#e6e6e6] text-[12px]'>{tool.name}</span>
</div>
)
})}
</div>
{/* Header */}
<div
className={`flex items-center justify-between p-2 ${hasContent ? 'border-[#3d3d3d] border-b' : ''}`}
>
<div className='relative z-10 flex min-w-0 flex-1 items-center gap-2.5'>
<div
className='flex size-[24px] flex-shrink-0 items-center justify-center rounded-[6px]'
style={{ background: bgColor }}
>
{Icon && <Icon className='size-[16px] text-white' />}
</div>
)}
<span className='truncate font-medium text-[#e6e6e6] text-[16px]'>{name}</span>
</div>
</div>
)}
{/* Source handle (right side) */}
{!hideSourceHandle && (
<Handle
type='source'
position={Position.Right}
id='source'
className={HANDLE_RIGHT}
style={{ top: '20px', transform: 'translateY(-50%)' }}
isConnectableStart={false}
isConnectableEnd={false}
/>
)}
{/* Sub-block rows + tools */}
{hasContent && (
<div className='flex flex-col gap-2 p-2'>
{rows.map((row) => {
const modelEntry = row.title === 'Model' ? getModelIconEntry(row.value) : null
const ModelIcon = modelEntry?.icon
return (
<div key={row.title} className='flex items-center gap-2'>
<span className='flex-shrink-0 font-normal text-[#b3b3b3] text-[14px] capitalize'>
{row.title}
</span>
{row.value && (
<span className='flex min-w-0 flex-1 items-center justify-end gap-2 font-normal text-[#e6e6e6] text-[14px]'>
{ModelIcon && (
<ModelIcon
className={`inline-block flex-shrink-0 text-[#e6e6e6] ${modelEntry.size ?? 'h-[14px] w-[14px]'}`}
/>
)}
<span className='truncate'>{row.value}</span>
</span>
)}
</div>
)
})}
{isHighlighted && (
<div className='pointer-events-none absolute inset-0 z-40 rounded-lg ring-[#33b4ff] ring-[1.75px]' />
)}
</div>
</motion.div>
{/* Tool chips — inline with label */}
{tools && tools.length > 0 && (
<div className='flex items-center gap-2'>
<span className='flex-shrink-0 font-normal text-[#b3b3b3] text-[14px]'>
Tools
</span>
<div className='flex flex-1 flex-wrap items-center justify-end gap-[5px]'>
{tools.map((tool) => {
const ToolIcon = BLOCK_ICONS[tool.type]
return (
<div
key={tool.type}
className='flex items-center gap-[5px] rounded-[5px] border border-[#3d3d3d] bg-[#2a2a2a] px-[6px] py-[3px]'
>
<div
className='flex size-[16px] flex-shrink-0 items-center justify-center rounded-[4px]'
style={{ background: tool.bgColor }}
>
{ToolIcon && <ToolIcon className='size-[10px] text-white' />}
</div>
<span className='font-normal text-[#e6e6e6] text-[12px]'>
{tool.name}
</span>
</div>
)
})}
</div>
</div>
)}
</div>
)}
{/* Source handle (right side) */}
{!hideSourceHandle && (
<Handle
type='source'
position={Position.Right}
id='source'
className={HANDLE_RIGHT}
style={{ top: '20px', transform: 'translateY(-50%)' }}
isConnectableStart={false}
isConnectableEnd={false}
/>
)}
{isHighlighted && (
<div className='pointer-events-none absolute inset-0 z-40 rounded-lg ring-[#33b4ff] ring-[1.75px]' />
)}
</div>
</m.div>
</LazyMotion>
)
})
@@ -1,7 +1,7 @@
'use client'
import { useCallback, useEffect, useRef, useState } from 'react'
import { AnimatePresence, motion, type Variants } from 'framer-motion'
import { AnimatePresence, domAnimation, LazyMotion, m, type Variants } from 'framer-motion'
import { LandingPreviewFiles } from '@/app/(landing)/components/landing-preview/components/landing-preview-files/landing-preview-files'
import { LandingPreviewHome } from '@/app/(landing)/components/landing-preview/components/landing-preview-home/landing-preview-home'
import { LandingPreviewKnowledge } from '@/app/(landing)/components/landing-preview/components/landing-preview-knowledge/landing-preview-knowledge'
@@ -208,115 +208,113 @@ export function LandingPreview() {
const isWorkflowView = activeView === 'workflow'
return (
<motion.div
className='dark flex aspect-[1116/615] w-full overflow-hidden rounded bg-[var(--landing-bg-surface)] antialiased'
initial={isDesktop ? 'hidden' : false}
animate='visible'
variants={containerVariants}
>
<motion.div className='hidden lg:flex' variants={sidebarVariants}>
<LandingPreviewSidebar
workflows={PREVIEW_WORKFLOWS}
activeWorkflowId={activeWorkflowId}
activeView={activeView}
onSelectWorkflow={handleSelectWorkflow}
onSelectHome={handleSelectHome}
onSelectNav={handleSelectNav}
/>
</motion.div>
<div className='flex min-w-0 flex-1 flex-col py-2 pr-2 pl-2 lg:pl-0'>
<div className='flex flex-1 overflow-hidden rounded-[5px] border border-[#2c2c2c] bg-[var(--landing-bg)]'>
<div
className={
isWorkflowView
? 'relative min-w-0 flex-1 overflow-hidden'
: 'relative flex min-w-0 flex-1 flex-col overflow-hidden'
}
>
{isDesktop ? (
<AnimatePresence mode='wait'>
{activeView === 'workflow' && (
<motion.div
key={`wf-${activeWorkflow.id}-${animationKey}`}
className='h-full w-full'
{...viewTransition}
>
<LandingPreviewWorkflow
workflow={activeWorkflow}
animate
highlightedBlockId={highlightedBlockId}
/>
</motion.div>
)}
{activeView === 'home' && (
<motion.div
key={`home-${animationKey}`}
className='flex h-full w-full flex-col'
{...viewTransition}
>
<LandingPreviewHome autoType={autoTypeHome} />
</motion.div>
)}
{activeView === 'tables' && (
<motion.div
key={`tables-${animationKey}`}
className='flex h-full w-full flex-col'
{...viewTransition}
>
<LandingPreviewTables autoOpenTableId={autoTableId} />
</motion.div>
)}
{activeView === 'files' && (
<motion.div
key='files'
className='flex h-full w-full flex-col'
{...viewTransition}
>
<LandingPreviewFiles />
</motion.div>
)}
{activeView === 'knowledge' && (
<motion.div
key='knowledge'
className='flex h-full w-full flex-col'
{...viewTransition}
>
<LandingPreviewKnowledge />
</motion.div>
)}
{activeView === 'logs' && (
<motion.div key='logs' className='flex h-full w-full flex-col' initial={false}>
<LandingPreviewLogs />
</motion.div>
)}
{activeView === 'scheduled-tasks' && (
<motion.div
key='scheduled-tasks'
className='flex h-full w-full flex-col'
{...viewTransition}
>
<LandingPreviewScheduledTasks />
</motion.div>
)}
</AnimatePresence>
) : (
<div className='h-full w-full'>
<LandingPreviewWorkflow workflow={activeWorkflow} />
</div>
)}
<LazyMotion features={domAnimation}>
<m.div
className='dark flex aspect-[1116/615] w-full overflow-hidden rounded bg-[var(--landing-bg-surface)] antialiased'
initial={isDesktop ? 'hidden' : false}
animate='visible'
variants={containerVariants}
>
<m.div className='hidden lg:flex' variants={sidebarVariants}>
<LandingPreviewSidebar
workflows={PREVIEW_WORKFLOWS}
activeWorkflowId={activeWorkflowId}
activeView={activeView}
onSelectWorkflow={handleSelectWorkflow}
onSelectHome={handleSelectHome}
onSelectNav={handleSelectNav}
/>
</m.div>
<div className='flex min-w-0 flex-1 flex-col py-2 pr-2 pl-2 lg:pl-0'>
<div className='flex flex-1 overflow-hidden rounded-[5px] border border-[#2c2c2c] bg-[var(--landing-bg)]'>
<div
className={
isWorkflowView
? 'relative min-w-0 flex-1 overflow-hidden'
: 'relative flex min-w-0 flex-1 flex-col overflow-hidden'
}
>
{isDesktop ? (
<AnimatePresence mode='wait'>
{activeView === 'workflow' && (
<m.div
key={`wf-${activeWorkflow.id}-${animationKey}`}
className='h-full w-full'
{...viewTransition}
>
<LandingPreviewWorkflow
workflow={activeWorkflow}
animate
highlightedBlockId={highlightedBlockId}
/>
</m.div>
)}
{activeView === 'home' && (
<m.div
key={`home-${animationKey}`}
className='flex h-full w-full flex-col'
{...viewTransition}
>
<LandingPreviewHome autoType={autoTypeHome} />
</m.div>
)}
{activeView === 'tables' && (
<m.div
key={`tables-${animationKey}`}
className='flex h-full w-full flex-col'
{...viewTransition}
>
<LandingPreviewTables autoOpenTableId={autoTableId} />
</m.div>
)}
{activeView === 'files' && (
<m.div key='files' className='flex h-full w-full flex-col' {...viewTransition}>
<LandingPreviewFiles />
</m.div>
)}
{activeView === 'knowledge' && (
<m.div
key='knowledge'
className='flex h-full w-full flex-col'
{...viewTransition}
>
<LandingPreviewKnowledge />
</m.div>
)}
{activeView === 'logs' && (
<m.div key='logs' className='flex h-full w-full flex-col' initial={false}>
<LandingPreviewLogs />
</m.div>
)}
{activeView === 'scheduled-tasks' && (
<m.div
key='scheduled-tasks'
className='flex h-full w-full flex-col'
{...viewTransition}
>
<LandingPreviewScheduledTasks />
</m.div>
)}
</AnimatePresence>
) : (
<div className='h-full w-full'>
<LandingPreviewWorkflow workflow={activeWorkflow} />
</div>
)}
</div>
<m.div
className={isWorkflowView ? 'hidden lg:flex' : 'hidden'}
variants={panelVariants}
>
<LandingPreviewPanel
activeWorkflow={activeWorkflow}
animationKey={animationKey}
onHighlightBlock={handleHighlightBlock}
/>
</m.div>
</div>
<motion.div
className={isWorkflowView ? 'hidden lg:flex' : 'hidden'}
variants={panelVariants}
>
<LandingPreviewPanel
activeWorkflow={activeWorkflow}
animationKey={animationKey}
onHighlightBlock={handleHighlightBlock}
/>
</motion.div>
</div>
</div>
</motion.div>
</m.div>
</LazyMotion>
)
}
@@ -78,7 +78,7 @@ export function DocsDropdown() {
className='flex flex-col gap-1 rounded-[5px] border border-[var(--landing-bg-elevated)] px-2.5 py-2 transition-colors hover:border-[var(--landing-border-strong)] hover:bg-[var(--landing-bg-card)]'
>
<div className='flex items-center gap-1.5'>
<Icon className='h-[13px] w-[13px] flex-shrink-0 text-[var(--landing-text-icon)]' />
<Icon className='size-[13px] flex-shrink-0 text-[var(--landing-text-icon)]' />
<span className='font-[430] font-season text-[var(--landing-text-body)] text-caption'>
{card.title}
</span>
@@ -20,7 +20,7 @@ export function GitHubStars() {
className='flex h-[30px] items-center gap-2 self-center rounded-[5px] px-3 transition-colors duration-200 group-hover:bg-[var(--landing-bg-elevated)]'
aria-label={`GitHub repository — ${stars} stars`}
>
<GithubOutlineIcon className='h-[14px] w-[14px]' />
<GithubOutlineIcon className='size-[14px]' />
<span aria-live='polite'>{stars}</span>
</a>
)
@@ -1,149 +0,0 @@
import type { ComponentType, SVGProps } from 'react'
import Link from 'next/link'
import {
AgentIcon,
ApiIcon,
McpIcon,
PackageSearchIcon,
TableIcon,
WorkflowIcon,
} from '@/components/icons'
interface ProductLink {
label: string
description: string
href: string
external?: boolean
icon: ComponentType<SVGProps<SVGSVGElement>>
}
interface SidebarLink {
label: string
href: string
external?: boolean
}
const WORKSPACE: ProductLink[] = [
{
label: 'Workflows',
description: 'Visual AI automation builder',
href: 'https://docs.sim.ai/getting-started',
external: true,
icon: WorkflowIcon,
},
{
label: 'Agent',
description: 'Build autonomous AI agents',
href: 'https://docs.sim.ai/blocks/agent',
external: true,
icon: AgentIcon,
},
{
label: 'MCP',
description: 'Connect external tools',
href: 'https://docs.sim.ai/mcp',
external: true,
icon: McpIcon,
},
{
label: 'Knowledge Base',
description: 'Retrieval-augmented context',
href: 'https://docs.sim.ai/knowledgebase',
external: true,
icon: PackageSearchIcon,
},
{
label: 'Tables',
description: 'Structured data storage',
href: 'https://docs.sim.ai/tables',
external: true,
icon: TableIcon,
},
{
label: 'API',
description: 'Deploy agents as endpoints',
href: 'https://docs.sim.ai/api-reference/getting-started',
external: true,
icon: ApiIcon,
},
]
const EXPLORE: SidebarLink[] = [
{ label: 'Models', href: '/models' },
{ label: 'Integrations', href: '/integrations' },
{ label: 'Changelog', href: '/changelog' },
{ label: 'Self-hosting', href: 'https://docs.sim.ai/self-hosting', external: true },
]
function DropdownLink({ link }: { link: ProductLink }) {
const Icon = link.icon
const Tag = link.external ? 'a' : Link
const props = link.external
? { href: link.href, target: '_blank' as const, rel: 'noopener noreferrer' }
: { href: link.href }
return (
<Tag
{...props}
className='group/item flex items-start gap-2.5 rounded-[5px] px-2.5 py-2 transition-colors hover:bg-[var(--landing-bg-elevated)]'
>
<Icon className='mt-0.5 h-[15px] w-[15px] shrink-0 text-[var(--landing-text-icon)]' />
<div className='flex flex-col'>
<span className='font-[430] font-season text-[13px] text-white leading-tight'>
{link.label}
</span>
<span className='font-season text-[12px] text-[var(--landing-text-subtle)] leading-[150%]'>
{link.description}
</span>
</div>
</Tag>
)
}
export function ProductDropdown() {
return (
<div className='flex w-[560px] rounded-[5px] border border-[var(--landing-bg-elevated)] bg-[var(--landing-bg)] shadow-overlay'>
<div className='flex-1 p-2'>
<div className='mb-1 px-2.5 pt-1'>
<span className='font-[430] font-season text-[11px] text-[var(--landing-text-subtle)] uppercase tracking-[0.08em]'>
Workspace
</span>
<div className='mt-1.5 h-px bg-[var(--landing-bg-elevated)]' />
</div>
<div className='grid grid-cols-2'>
{WORKSPACE.map((link) => (
<DropdownLink key={link.label} link={link} />
))}
</div>
</div>
<div className='w-px self-stretch bg-[var(--landing-bg-elevated)]' />
<div className='w-[160px] p-2'>
<div className='mb-1 px-2.5 pt-1'>
<span className='font-[430] font-season text-[11px] text-[var(--landing-text-subtle)] uppercase tracking-[0.08em]'>
Explore
</span>
<div className='mt-1.5 h-px bg-[var(--landing-bg-elevated)]' />
</div>
{EXPLORE.map((link) => {
const Tag = link.external ? 'a' : Link
const props = link.external
? { href: link.href, target: '_blank' as const, rel: 'noopener noreferrer' }
: { href: link.href }
return (
<Tag
key={link.label}
{...props}
className='block rounded-[5px] px-2.5 py-1.5 font-[430] font-season text-[13px] text-white transition-colors hover:bg-[var(--landing-bg-elevated)]'
>
{link.label}
</Tag>
)
})}
</div>
</div>
)
}
@@ -43,22 +43,28 @@ const LOGO_CELL = 'flex items-center pl-5 lg:pl-16 pr-5'
const LINK_CELL = 'flex items-center px-3.5'
const emptySubscribe = () => () => {}
const getLocationSearch = () => window.location.search
const getServerLocationSearch = () => ''
const EMPTY_BLOG_POSTS: NavBlogPost[] = []
interface NavbarProps {
logoOnly?: boolean
blogPosts?: NavBlogPost[]
}
export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps) {
export default function Navbar({ logoOnly = false, blogPosts = EMPTY_BLOG_POSTS }: NavbarProps) {
const brand = getBrandConfig()
const sessionCtx = useContext(SessionContext)
const session = sessionCtx?.data ?? null
const isSessionPending = sessionCtx?.isPending ?? true
const isAuthenticated = Boolean(session?.user?.id)
const [isBrowsingHome, setIsBrowsingHome] = useState(false)
useEffect(() => {
setIsBrowsingHome(new URLSearchParams(window.location.search).has('home'))
}, [])
const locationSearch = useSyncExternalStore(
emptySubscribe,
getLocationSearch,
getServerLocationSearch
)
const isBrowsingHome = new URLSearchParams(locationSearch).has('home')
const useHomeLinks = isAuthenticated || isBrowsingHome
const logoHref = useHomeLinks ? '/?home' : '/'
const mounted = useSyncExternalStore(
@@ -285,7 +291,7 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps
<div className='flex flex-1 items-center justify-end pr-5 lg:hidden'>
<button
type='button'
className='flex h-[32px] w-[32px] items-center justify-center rounded-[5px] transition-colors hover:bg-[var(--landing-bg-elevated)]'
className='flex size-[32px] items-center justify-center rounded-[5px] transition-colors hover:bg-[var(--landing-bg-elevated)]'
onClick={() => setMobileMenuOpen((prev) => !prev)}
aria-label={mobileMenuOpen ? 'Close menu' : 'Open menu'}
aria-expanded={mobileMenuOpen}
@@ -337,7 +343,7 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps
className='flex items-center gap-2 px-5 py-3.5 text-[var(--landing-text)] transition-colors active:bg-[var(--landing-bg-elevated)]'
onClick={() => setMobileMenuOpen(false)}
>
<GithubOutlineIcon className='h-[14px] w-[14px]' />
<GithubOutlineIcon className='size-[14px]' />
GitHub
</a>
</li>
@@ -2,7 +2,7 @@
import { useCallback, useEffect, useState } from 'react'
import { createLogger } from '@sim/logger'
import { AnimatePresence, motion } from 'framer-motion'
import { AnimatePresence, domAnimation, LazyMotion, m } from 'framer-motion'
import dynamic from 'next/dynamic'
import { useRouter } from 'next/navigation'
import { Badge, ChevronDown } from '@/components/emcn'
@@ -315,228 +315,234 @@ export default function Templates() {
])
return (
<section id='templates' aria-labelledby='templates-heading' className='pt-[60px] lg:pt-[100px]'>
<p className='sr-only'>
Sim includes {TEMPLATE_WORKFLOWS.length} pre-built workflow templates covering OCR
processing, release management, meeting follow-ups, resume scanning, email triage,
competitor monitoring, social listening, data enrichment, feedback analysis, code review,
and knowledge base Q&amp;A. Each template connects real integrations and LLMs pick one,
customise it, and deploy in minutes.
</p>
<ul className='sr-only'>
{TEMPLATE_WORKFLOWS.map((workflow) => (
<li key={workflow.id}>{workflow.name}</li>
))}
</ul>
<LazyMotion features={domAnimation}>
<section
id='templates'
aria-labelledby='templates-heading'
className='pt-[60px] lg:pt-[100px]'
>
<p className='sr-only'>
Sim includes {TEMPLATE_WORKFLOWS.length} pre-built workflow templates covering OCR
processing, release management, meeting follow-ups, resume scanning, email triage,
competitor monitoring, social listening, data enrichment, feedback analysis, code review,
and knowledge base Q&amp;A. Each template connects real integrations and LLMs; pick one,
customise it, and deploy in minutes.
</p>
<ul className='sr-only'>
{TEMPLATE_WORKFLOWS.map((workflow) => (
<li key={workflow.id}>{workflow.name}</li>
))}
</ul>
<div className='bg-[var(--landing-bg)]'>
<div className='relative overflow-hidden'>
<div className='px-5 lg:px-16'>
<div className='flex flex-col items-start gap-5'>
<Badge
variant='blue'
size='md'
dot
className='font-season uppercase tracking-[0.02em] transition-colors duration-200'
style={{
color: activeDepth.color,
backgroundColor: hexToRgba(activeDepth.color, 0.1),
}}
>
Templates
</Badge>
<h2
id='templates-heading'
className='text-balance font-[430] font-season text-[28px] text-white leading-[100%] tracking-[-0.02em] lg:text-[40px]'
>
Ship your agent in minutes
</h2>
<p className='font-[430] font-season text-[#F6F6F0]/50 text-base leading-[150%] tracking-[0.02em] lg:text-lg'>
Pre-built templates for every use casepick one, swap{' '}
<br className='hidden lg:inline' />
models and tools to fit your stack, and deploy.
</p>
</div>
</div>
<div className='mt-10 flex border-[var(--landing-bg-elevated)] border-y lg:mt-[73px]'>
<div
aria-hidden='true'
className='w-[24px] shrink-0 border-[var(--landing-bg-elevated)] border-r lg:w-16'
/>
<div className='flex min-w-0 flex-1 flex-col lg:flex-row'>
<div
role='tablist'
aria-label='Workflow templates'
className='flex w-full shrink-0 flex-col border-[var(--landing-bg-elevated)] lg:w-[300px] lg:border-r'
>
{TEMPLATE_WORKFLOWS.map((workflow, index) => {
const isActive = index === activeIndex
return (
<div key={workflow.id}>
<button
id={`template-tab-${index}`}
type='button'
role='tab'
aria-selected={isActive}
aria-controls={TEMPLATES_PANEL_ID}
onClick={() => setActiveIndex(index)}
className={cn(
'relative w-full text-left',
isActive
? 'z-10'
: cn(
'flex items-center px-3 py-2.5 hover:bg-[color-mix(in_srgb,var(--landing-bg-card)_50%,transparent)]',
index < TEMPLATE_WORKFLOWS.length - 1 &&
'shadow-[inset_0_-1px_0_0_#2A2A2A]'
)
)}
>
{isActive ? (
(() => {
const depth = DEPTH_CONFIGS[workflow.id]
return (
<>
<div
className='absolute top-[-8px] bottom-0 left-0 w-2'
style={{
clipPath: LEFT_WALL_CLIP,
backgroundColor: hexToRgba(depth.color, 0.63),
}}
/>
<div
className='absolute right-[-8px] bottom-0 left-2 h-2'
style={buildBottomWallStyle(depth)}
/>
<div className='-translate-y-2 relative flex translate-x-2 items-center bg-[var(--landing-bg-card)] px-3 py-2.5 shadow-[inset_0_0_0_1.5px_#3E3E3E]'>
<span className='flex-1 font-[430] font-season text-md text-white'>
{workflow.name}
</span>
<ChevronDown
className='-rotate-90 h-[11px] w-[11px] shrink-0'
style={{ color: depth.color }}
/>
</div>
</>
)
})()
) : (
<span className='font-[430] font-season text-[#F6F6F0]/50 text-md'>
{workflow.name}
</span>
)}
</button>
<AnimatePresence>
{isActive && isMobile && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.25, ease: [0.4, 0, 0.2, 1] }}
className='overflow-hidden'
>
<div className='aspect-[16/10] w-full border-[var(--landing-bg-elevated)] border-y bg-[var(--landing-bg)]'>
<LandingPreviewWorkflow
workflow={workflow}
animate
fitViewOptions={{ padding: 0.15, minZoom: 0.1, maxZoom: 0.8 }}
/>
</div>
<div className='p-3'>
<button
type='button'
onClick={handleUseTemplate}
disabled={isPreparingTemplate}
className='inline-flex h-[32px] w-full cursor-pointer items-center justify-center gap-1.5 rounded-[5px] border border-white bg-white font-[430] font-season text-black text-sm transition-colors active:bg-[#E0E0E0]'
>
{isPreparingTemplate ? 'Preparing...' : 'Use template'}
</button>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
)
})}
</div>
<div
id={TEMPLATES_PANEL_ID}
role='tabpanel'
aria-labelledby={`template-tab-${activeIndex}`}
className='relative hidden flex-1 lg:block'
>
<div aria-hidden='true' inert className='h-full'>
<LandingPreviewWorkflow
key={activeIndex}
workflow={activeWorkflow}
animate
fitViewOptions={{ padding: 0.15, maxZoom: 1.3 }}
/>
</div>
<button
type='button'
onClick={handleUseTemplate}
disabled={isPreparingTemplate}
className='group/cta absolute top-4 right-[16px] z-10 inline-flex h-[32px] cursor-pointer items-center gap-1.5 rounded-[5px] border border-white bg-white px-2.5 font-[430] font-season text-black text-sm transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
<div className='bg-[var(--landing-bg)]'>
<div className='relative overflow-hidden'>
<div className='px-5 lg:px-16'>
<div className='flex flex-col items-start gap-5'>
<Badge
variant='blue'
size='md'
dot
className='font-season uppercase tracking-[0.02em] transition-colors duration-200'
style={{
color: activeDepth.color,
backgroundColor: hexToRgba(activeDepth.color, 0.1),
}}
>
{isPreparingTemplate ? 'Preparing...' : 'Use template'}
<svg
className='h-[10px] w-[10px] shrink-0'
viewBox='0 0 10 10'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<line
x1='0'
y1='5'
x2='9'
y2='5'
stroke='currentColor'
strokeWidth='1.33'
strokeLinecap='square'
className='origin-left scale-x-0 transition-transform duration-200 ease-out [transform-box:fill-box] group-hover/cta:scale-x-100'
/>
<path
d='M3.5 2L6.5 5L3.5 8'
stroke='currentColor'
strokeWidth='1.33'
strokeLinecap='square'
strokeLinejoin='miter'
fill='none'
className='transition-transform duration-200 ease-out group-hover/cta:translate-x-[30%]'
/>
</svg>
</button>
Templates
</Badge>
<h2
id='templates-heading'
className='text-balance font-[430] font-season text-[28px] text-white leading-[100%] tracking-[-0.02em] lg:text-[40px]'
>
Ship your agent in minutes
</h2>
<p className='font-[430] font-season text-[#F6F6F0]/50 text-base leading-[150%] tracking-[0.02em] lg:text-lg'>
Pre-built templates for every use case: pick one, swap{' '}
<br className='hidden lg:inline' />
models and tools to fit your stack, and deploy.
</p>
</div>
</div>
<div
aria-hidden='true'
className='w-[24px] shrink-0 border-[var(--landing-bg-elevated)] border-l lg:w-16'
/>
</div>
<div className='mt-10 flex border-[var(--landing-bg-elevated)] border-y lg:mt-[73px]'>
<div
aria-hidden='true'
className='w-[24px] shrink-0 border-[var(--landing-bg-elevated)] border-r lg:w-16'
/>
<div className='relative pb-[60px] lg:pb-[100px]'>
<div
aria-hidden='true'
className='absolute top-0 bottom-0 left-[calc(4rem-1px)] hidden w-px bg-[var(--landing-bg-elevated)] lg:block'
/>
<div
aria-hidden='true'
className='absolute top-0 right-[calc(4rem-1px)] bottom-0 hidden w-px bg-[var(--landing-bg-elevated)] lg:block'
/>
<div
aria-hidden='true'
className='absolute right-16 bottom-0 left-16 hidden h-px bg-[var(--landing-bg-elevated)] lg:block'
/>
<div className='flex min-w-0 flex-1 flex-col lg:flex-row'>
<div
role='tablist'
aria-label='Workflow templates'
className='flex w-full shrink-0 flex-col border-[var(--landing-bg-elevated)] lg:w-[300px] lg:border-r'
>
{TEMPLATE_WORKFLOWS.map((workflow, index) => {
const isActive = index === activeIndex
return (
<div key={workflow.id}>
<button
id={`template-tab-${index}`}
type='button'
role='tab'
aria-selected={isActive}
aria-controls={TEMPLATES_PANEL_ID}
onClick={() => setActiveIndex(index)}
className={cn(
'relative w-full text-left',
isActive
? 'z-10'
: cn(
'flex items-center px-3 py-2.5 hover:bg-[color-mix(in_srgb,var(--landing-bg-card)_50%,transparent)]',
index < TEMPLATE_WORKFLOWS.length - 1 &&
'shadow-[inset_0_-1px_0_0_#2A2A2A]'
)
)}
>
{isActive ? (
(() => {
const depth = DEPTH_CONFIGS[workflow.id]
return (
<>
<div
className='absolute top-[-8px] bottom-0 left-0 w-2'
style={{
clipPath: LEFT_WALL_CLIP,
backgroundColor: hexToRgba(depth.color, 0.63),
}}
/>
<div
className='absolute right-[-8px] bottom-0 left-2 h-2'
style={buildBottomWallStyle(depth)}
/>
<div className='-translate-y-2 relative flex translate-x-2 items-center bg-[var(--landing-bg-card)] px-3 py-2.5 shadow-[inset_0_0_0_1.5px_#3E3E3E]'>
<span className='flex-1 font-[430] font-season text-md text-white'>
{workflow.name}
</span>
<ChevronDown
className='-rotate-90 size-[11px] shrink-0'
style={{ color: depth.color }}
/>
</div>
</>
)
})()
) : (
<span className='font-[430] font-season text-[#F6F6F0]/50 text-md'>
{workflow.name}
</span>
)}
</button>
<AnimatePresence>
{isActive && isMobile && (
<m.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.25, ease: [0.4, 0, 0.2, 1] }}
className='overflow-hidden'
>
<div className='aspect-[16/10] w-full border-[var(--landing-bg-elevated)] border-y bg-[var(--landing-bg)]'>
<LandingPreviewWorkflow
workflow={workflow}
animate
fitViewOptions={{ padding: 0.15, minZoom: 0.1, maxZoom: 0.8 }}
/>
</div>
<div className='p-3'>
<button
type='button'
onClick={handleUseTemplate}
disabled={isPreparingTemplate}
className='inline-flex h-[32px] w-full cursor-pointer items-center justify-center gap-1.5 rounded-[5px] border border-white bg-white font-[430] font-season text-black text-sm transition-colors active:bg-[#E0E0E0]'
>
{isPreparingTemplate ? 'Preparing...' : 'Use template'}
</button>
</div>
</m.div>
)}
</AnimatePresence>
</div>
)
})}
</div>
<div
id={TEMPLATES_PANEL_ID}
role='tabpanel'
aria-labelledby={`template-tab-${activeIndex}`}
className='relative hidden flex-1 lg:block'
>
<div aria-hidden='true' inert className='h-full'>
<LandingPreviewWorkflow
key={activeIndex}
workflow={activeWorkflow}
animate
fitViewOptions={{ padding: 0.15, maxZoom: 1.3 }}
/>
</div>
<button
type='button'
onClick={handleUseTemplate}
disabled={isPreparingTemplate}
className='group/cta absolute top-4 right-[16px] z-10 inline-flex h-[32px] cursor-pointer items-center gap-1.5 rounded-[5px] border border-white bg-white px-2.5 font-[430] font-season text-black text-sm transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
>
{isPreparingTemplate ? 'Preparing...' : 'Use template'}
<svg
className='size-[10px] shrink-0'
viewBox='0 0 10 10'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<line
x1='0'
y1='5'
x2='9'
y2='5'
stroke='currentColor'
strokeWidth='1.33'
strokeLinecap='square'
className='origin-left scale-x-0 transition-transform duration-200 ease-out [transform-box:fill-box] group-hover/cta:scale-x-100'
/>
<path
d='M3.5 2L6.5 5L3.5 8'
stroke='currentColor'
strokeWidth='1.33'
strokeLinecap='square'
strokeLinejoin='miter'
fill='none'
className='transition-transform duration-200 ease-out group-hover/cta:translate-x-[30%]'
/>
</svg>
</button>
</div>
</div>
<div
aria-hidden='true'
className='w-[24px] shrink-0 border-[var(--landing-bg-elevated)] border-l lg:w-16'
/>
</div>
<div className='relative pb-[60px] lg:pb-[100px]'>
<div
aria-hidden='true'
className='absolute top-0 bottom-0 left-[calc(4rem-1px)] hidden w-px bg-[var(--landing-bg-elevated)] lg:block'
/>
<div
aria-hidden='true'
className='absolute top-0 right-[calc(4rem-1px)] bottom-0 hidden w-px bg-[var(--landing-bg-elevated)] lg:block'
/>
<div
aria-hidden='true'
className='absolute right-16 bottom-0 left-16 hidden h-px bg-[var(--landing-bg-elevated)] lg:block'
/>
</div>
</div>
</div>
</div>
</section>
</section>
</LazyMotion>
)
}
@@ -14,14 +14,18 @@ interface TemplateCardButtonProps {
export function TemplateCardButton({ prompt, className, children }: TemplateCardButtonProps) {
const router = useRouter()
function handleClick() {
function savePromptAndNavigate() {
LandingPromptStorage.store(prompt)
trackLandingCta({ label: 'Template card', section: 'integrations', destination: '/signup' })
router.push('/signup')
}
return (
<button type='button' onClick={handleClick} className={cn('w-full text-left', className)}>
<button
type='button'
onClick={savePromptAndNavigate}
className={cn('w-full text-left', className)}
>
{children}
</button>
)
@@ -3,7 +3,7 @@ import { Loader } from '@/components/emcn'
export default function IntegrationDetailLoading() {
return (
<div className='flex min-h-[60vh] items-center justify-center bg-[var(--landing-bg)]'>
<Loader animate className='h-6 w-6 text-[var(--landing-text-muted)]' />
<Loader animate className='size-6 text-[var(--landing-text-muted)]' />
</div>
)
}
@@ -321,7 +321,7 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
className='group/link inline-flex items-center gap-1.5 font-season text-[var(--landing-text-muted)] text-sm tracking-[0.02em] hover:text-[var(--landing-text)]'
>
<svg
className='h-3 w-3 shrink-0'
className='size-3 shrink-0'
viewBox='0 0 10 10'
fill='none'
xmlns='http://www.w3.org/2000/svg'
@@ -357,7 +357,7 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
bgColor={bgColor}
name={name}
Icon={IconComponent}
className='h-12 w-12 rounded-[5px]'
className='size-12 rounded-[5px]'
iconClassName='h-6 w-6'
fallbackClassName='text-[20px]'
aria-hidden='true'
@@ -393,7 +393,7 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
View docs
<svg
aria-hidden='true'
className='-rotate-45 h-3 w-3 shrink-0'
className='-rotate-45 size-3 shrink-0'
viewBox='0 0 10 10'
fill='none'
>
@@ -477,7 +477,7 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
].map(({ step, title, body }) => (
<li key={step} className='flex gap-4'>
<span
className='mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-[var(--landing-border-strong)] font-martian-mono text-[11px] text-[var(--landing-text-subtle)]'
className='mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-full border border-[var(--landing-border-strong)] font-martian-mono text-[11px] text-[var(--landing-text-subtle)]'
aria-hidden='true'
>
{step}
@@ -500,9 +500,9 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
<section aria-labelledby='triggers-heading'>
<div className='px-6 pt-10 pb-4'>
<div className='mb-2 flex items-center gap-2.5'>
<span className='relative flex h-2 w-2' aria-hidden='true'>
<span className='relative flex size-2' aria-hidden='true'>
<span className='absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75' />
<span className='relative inline-flex h-2 w-2 rounded-full bg-emerald-500' />
<span className='relative inline-flex size-2 rounded-full bg-emerald-500' />
</span>
<h2
id='triggers-heading'
@@ -512,8 +512,8 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
</h2>
</div>
<p className='text-[14px] text-[var(--landing-text-body)] leading-[150%] tracking-[0.02em]'>
Connect a {name} webhook to Sim and your agent runs the instant an event happens
no polling, no delay.
Connect a {name} webhook to Sim and your agent runs the instant an event happens, no
polling, no delay.
</p>
</div>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
@@ -585,7 +585,7 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
name={int?.name ?? bt}
Icon={ToolIcon}
as='span'
className='h-6 w-6 rounded-[4px]'
className='size-6 rounded-[4px]'
iconClassName='h-3.5 w-3.5'
fallbackClassName='text-[10px]'
aria-hidden='true'
@@ -721,7 +721,7 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
name={rel.name}
Icon={blockTypeToIconMap[rel.type]}
as='span'
className='h-10 w-10 rounded-[5px]'
className='size-10 rounded-[5px]'
aria-hidden='true'
/>
<div className='flex flex-col gap-2'>
@@ -753,11 +753,11 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
<div className='flex items-center gap-2'>
<span className='h-px w-5 bg-[#3d3d3d]' aria-hidden='true' />
<span
className='flex h-7 w-7 items-center justify-center rounded-full border border-[var(--landing-border-strong)]'
className='flex size-7 items-center justify-center rounded-full border border-[var(--landing-border-strong)]'
aria-hidden='true'
>
<svg
className='h-3.5 w-3.5 text-[var(--landing-text-secondary)]'
className='size-3.5 text-[var(--landing-text-secondary)]'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
@@ -774,7 +774,7 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
bgColor={bgColor}
name={name}
Icon={IconComponent}
className='h-14 w-14 rounded-xl'
className='size-14 rounded-xl'
iconClassName='h-7 w-7'
fallbackClassName='text-[22px]'
aria-hidden='true'
@@ -788,7 +788,7 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
</h2>
<p className='mx-auto mb-8 max-w-[480px] text-[var(--landing-text-body)] text-base leading-[150%] tracking-[0.02em]'>
Build your first AI agent with {name} in minutes. Connect to every tool your team uses.
Free to start no credit card required.
Free to start, no credit card required.
</p>
<IntegrationCtaButton
label='Build for free'
@@ -170,7 +170,7 @@ export default function IntegrationsPage() {
</section>
{/* Integration request */}
<div className='flex flex-col items-start gap-3 px-6 py-6 sm:flex-row sm:items-center sm:justify-between'>
<div className='flex flex-col items-start gap-3 p-6 sm:flex-row sm:items-center sm:justify-between'>
<div>
<p className='text-[15px] text-white tracking-[-0.02em]'>
Don&apos;t see the integration you need?
@@ -3,7 +3,7 @@ import { Loader } from '@/components/emcn'
export default function IntegrationDetailLoading() {
return (
<div className='flex min-h-[60vh] items-center justify-center bg-[var(--landing-bg)]'>
<Loader animate className='h-6 w-6 text-[var(--landing-text-muted)]' />
<Loader animate className='size-6 text-[var(--landing-text-muted)]' />
</div>
)
}
@@ -25,7 +25,7 @@ export function IntegrationCard({ integration, IconComponent }: IntegrationCardP
bgColor={bgColor}
name={name}
Icon={IconComponent}
className='h-10 w-10 rounded-[5px]'
className='size-10 rounded-[5px]'
aria-hidden='true'
/>
<div className='flex flex-col gap-2'>
@@ -61,7 +61,7 @@ export function IntegrationRow({ integration, IconComponent }: IntegrationRowPro
bgColor={bgColor}
name={name}
Icon={IconComponent}
className='h-8 w-8 shrink-0 rounded-[5px]'
className='size-8 shrink-0 rounded-[5px]'
iconClassName='h-4 w-4'
fallbackClassName='text-[13px]'
aria-hidden='true'
@@ -78,7 +78,7 @@ export function IntegrationGrid({ integrations }: IntegrationGridProps) {
<div className='relative max-w-[480px] flex-1'>
<svg
aria-hidden='true'
className='-translate-y-1/2 pointer-events-none absolute top-1/2 left-3 h-4 w-4 text-[#555]'
className='-translate-y-1/2 pointer-events-none absolute top-1/2 left-3 size-4 text-[#555]'
fill='none'
stroke='currentColor'
strokeWidth={2}
@@ -84,9 +84,9 @@ export function RequestIntegrationModal() {
{status === 'success' ? (
<ModalBody>
<div className='flex flex-col items-center gap-3 py-6 text-center'>
<div className='flex h-10 w-10 items-center justify-center rounded-full bg-[#33C482]/10'>
<div className='flex size-10 items-center justify-center rounded-full bg-[#33C482]/10'>
<svg
className='h-5 w-5 text-[var(--brand-accent)]'
className='size-5 text-[var(--brand-accent)]'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
@@ -98,7 +98,7 @@ export function RequestIntegrationModal() {
</svg>
</div>
<p className='text-[14px] text-[var(--landing-text)]'>
Request submitted we&apos;ll follow up at{' '}
Request submitted. We&apos;ll follow up at{' '}
<span className='font-medium'>{email}</span>.
</p>
</div>
@@ -3,13 +3,13 @@
export type AuthType = 'oauth' | 'api-key' | 'none'
export interface TriggerInfo {
interface TriggerInfo {
id: string
name: string
description: string
}
export interface OperationInfo {
interface OperationInfo {
name: string
description: string
}
@@ -3,7 +3,7 @@ import { Loader } from '@/components/emcn'
export default function ModelDetailLoading() {
return (
<div className='flex min-h-[60vh] items-center justify-center bg-[var(--landing-bg)]'>
<Loader animate className='h-6 w-6 text-[var(--landing-text-muted)]' />
<Loader animate className='size-6 text-[var(--landing-text-muted)]' />
</div>
)
}
@@ -168,7 +168,7 @@ export default async function ModelPage({
className='group/link inline-flex items-center gap-1.5 font-season text-[var(--landing-text-muted)] text-sm tracking-[0.02em] hover:text-[var(--landing-text)]'
>
<svg
className='h-3 w-3 shrink-0'
className='size-3 shrink-0'
viewBox='0 0 10 10'
fill='none'
xmlns='http://www.w3.org/2000/svg'
@@ -200,7 +200,7 @@ export default async function ModelPage({
<div className='mb-6 flex items-center gap-5'>
<ProviderIcon
provider={provider}
className='h-16 w-16 rounded-[5px]'
className='size-16 rounded-[5px]'
iconClassName='h-8 w-8'
/>
<div>
@@ -222,12 +222,12 @@ export default async function ModelPage({
</p>
<div className='flex flex-wrap gap-2'>
<a
<Link
href='/'
className='inline-flex h-[32px] items-center gap-2 rounded-[5px] border border-white bg-white px-2.5 font-season text-black text-sm transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
>
Build with this model
</a>
</Link>
<Link
href={provider.href}
className='inline-flex h-[32px] items-center rounded-[5px] border border-[var(--landing-border-strong)] px-2.5 font-season text-[var(--landing-text)] text-sm transition-colors hover:bg-[var(--landing-bg-elevated)]'
@@ -3,7 +3,7 @@ import { Loader } from '@/components/emcn'
export default function ModelProviderLoading() {
return (
<div className='flex min-h-[60vh] items-center justify-center bg-[var(--landing-bg)]'>
<Loader animate className='h-6 w-6 text-[var(--landing-text-muted)]' />
<Loader animate className='size-6 text-[var(--landing-text-muted)]' />
</div>
)
}
@@ -164,7 +164,7 @@ export default async function ProviderModelsPage({
className='group/link inline-flex items-center gap-1.5 font-season text-[var(--landing-text-muted)] text-sm tracking-[0.02em] hover:text-[var(--landing-text)]'
>
<svg
className='h-3 w-3 shrink-0'
className='size-3 shrink-0'
viewBox='0 0 10 10'
fill='none'
xmlns='http://www.w3.org/2000/svg'
@@ -206,7 +206,7 @@ export default async function ProviderModelsPage({
<div className='flex items-center gap-4'>
<ProviderIcon
provider={provider}
className='h-12 w-12 rounded-[5px]'
className='size-12 rounded-[5px]'
iconClassName='h-6 w-6'
/>
<h1
@@ -3,7 +3,7 @@ import { Loader } from '@/components/emcn'
export default function ModelDetailLoading() {
return (
<div className='flex min-h-[60vh] items-center justify-center bg-[var(--landing-bg)]'>
<Loader animate className='h-6 w-6 text-[var(--landing-text-muted)]' />
<Loader animate className='size-6 text-[var(--landing-text-muted)]' />
</div>
)
}
@@ -3,7 +3,7 @@ import { Loader } from '@/components/emcn'
export default function ModelProviderLoading() {
return (
<div className='flex min-h-[60vh] items-center justify-center bg-[var(--landing-bg)]'>
<Loader animate className='h-6 w-6 text-[var(--landing-text-muted)]' />
<Loader animate className='size-6 text-[var(--landing-text-muted)]' />
</div>
)
}
@@ -60,7 +60,7 @@ function ModelLabel({ model }: ModelLabelProps) {
return (
<div className='flex w-[90px] shrink-0 items-center justify-end gap-1.5 sm:w-[140px] lg:w-[180px]'>
{Icon && <Icon className='h-3.5 w-3.5 shrink-0' />}
{Icon && <Icon className='size-3.5 shrink-0' />}
<span className='truncate font-medium text-[13px] text-[var(--landing-text)] leading-none tracking-[-0.01em]'>
{model.displayName}
</span>
@@ -85,7 +85,7 @@ export function ModelDirectory() {
<div className='relative max-w-[480px] flex-1'>
<svg
aria-hidden='true'
className='-translate-y-1/2 pointer-events-none absolute top-1/2 left-3 h-4 w-4 text-[#555]'
className='-translate-y-1/2 pointer-events-none absolute top-1/2 left-3 size-4 text-[#555]'
fill='none'
stroke='currentColor'
strokeWidth={2}
@@ -157,7 +157,7 @@ export function ModelDirectory() {
>
<ProviderIcon
provider={provider}
className='h-8 w-8 rounded-[5px]'
className='size-8 rounded-[5px]'
iconClassName='h-4 w-4'
/>
<div className='min-w-0 flex-1'>
@@ -206,7 +206,7 @@ export function ModelDirectory() {
>
<ProviderIcon
provider={provider}
className='h-8 w-8 rounded-[5px]'
className='size-8 rounded-[5px]'
iconClassName='h-4 w-4'
/>
<div className='min-w-0 flex-1'>
@@ -236,7 +236,7 @@ function ModelRow({ provider, model }: { provider: CatalogProvider; model: Catal
>
<ProviderIcon
provider={provider}
className='h-8 w-8 shrink-0 rounded-[5px]'
className='size-8 shrink-0 rounded-[5px]'
iconClassName='h-4 w-4'
/>
@@ -100,7 +100,7 @@ export function DetailItem({ label, value }: { label: string; value: string }) {
export function ChevronArrow() {
return (
<svg
className='h-3 w-3 shrink-0 text-[var(--landing-text-subtle)]'
className='size-3 shrink-0 text-[var(--landing-text-subtle)]'
viewBox='0 0 10 10'
fill='none'
xmlns='http://www.w3.org/2000/svg'
@@ -154,11 +154,7 @@ export function FeaturedProviderCard({ provider }: { provider: CatalogProvider }
href={provider.href}
className='group flex flex-1 flex-col gap-4 border-[var(--landing-bg-elevated)] border-t p-6 transition-colors first:border-t-0 hover:bg-[var(--landing-bg-elevated)] sm:border-t-0 sm:border-l sm:first:border-l-0'
>
<ProviderIcon
provider={provider}
className='h-10 w-10 rounded-[5px]'
iconClassName='h-5 w-5'
/>
<ProviderIcon provider={provider} className='size-10 rounded-[5px]' iconClassName='h-5 w-5' />
<div className='flex flex-col gap-2'>
<h3 className='text-lg text-white leading-tight tracking-[-0.01em]'>{provider.name}</h3>
<p className='line-clamp-2 text-[var(--landing-text-muted)] text-sm leading-[150%]'>
@@ -181,11 +177,7 @@ export function FeaturedModelCard({
href={model.href}
className='group flex flex-1 flex-col gap-4 border-[var(--landing-bg-elevated)] border-t p-6 transition-colors first:border-t-0 hover:bg-[var(--landing-bg-elevated)] sm:border-t-0 sm:border-l sm:first:border-l-0'
>
<ProviderIcon
provider={provider}
className='h-10 w-10 rounded-[5px]'
iconClassName='h-5 w-5'
/>
<ProviderIcon provider={provider} className='size-10 rounded-[5px]' iconClassName='h-5 w-5' />
<div className='flex flex-col gap-2'>
<span className='font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'>
{provider.name}
@@ -255,7 +247,7 @@ export function ModelCard({
<div className='mb-4 flex items-start gap-3'>
<ProviderIcon
provider={provider}
className='h-10 w-10 rounded-[5px]'
className='size-10 rounded-[5px]'
iconClassName='h-5 w-5'
/>
<div className='min-w-0 flex-1'>
@@ -83,7 +83,7 @@ export function ModelTimelineChart({ models, providerId }: ModelTimelineChartPro
>
{/* Dot — centered exactly on the line (70px - 4.5px) */}
<div
className='-translate-x-1/2 absolute top-[66px] left-1/2 h-[9px] w-[9px] rounded-full transition-[filter,transform] duration-150 group-hover:scale-150 group-hover:brightness-150'
className='-translate-x-1/2 absolute top-[66px] left-1/2 size-[9px] rounded-full transition-[filter,transform] duration-150 group-hover:scale-150 group-hover:brightness-150'
style={{ backgroundColor: color, opacity: 0.85 }}
/>
+5 -3
View File
@@ -780,12 +780,14 @@ export function buildModelCapabilityFacts(model: CatalogModel): CapabilityFact[]
}
export function getCheapestProviderModel(provider: CatalogProvider): CatalogModel | null {
return [...provider.models].sort((a, b) => a.pricing.input - b.pricing.input)[0] ?? null
if (provider.models.length === 0) return null
return provider.models.reduce((min, m) => (m.pricing.input < min.pricing.input ? m : min))
}
export function getLargestContextProviderModel(provider: CatalogProvider): CatalogModel | null {
return (
[...provider.models].sort((a, b) => (b.contextWindow ?? 0) - (a.contextWindow ?? 0))[0] ?? null
if (provider.models.length === 0) return null
return provider.models.reduce((max, m) =>
(m.contextWindow ?? 0) > (max.contextWindow ?? 0) ? m : max
)
}
+3 -3
View File
@@ -242,7 +242,7 @@ export default async function PartnersPage() {
<ul className='space-y-1.5'>
{tier.requirements.map((r) => (
<li key={r} className='flex items-start gap-2 text-[#999] text-[13px]'>
<span className='mt-1.5 h-1 w-1 flex-shrink-0 rounded-full bg-[#555]' />
<span className='mt-1.5 size-1 flex-shrink-0 rounded-full bg-[#555]' />
{r}
</li>
))}
@@ -254,7 +254,7 @@ export default async function PartnersPage() {
<ul className='space-y-1.5'>
{tier.perks.map((p) => (
<li key={p} className='flex items-start gap-2 text-[#ECECEC] text-[13px]'>
<span className='mt-1.5 h-1 w-1 flex-shrink-0 rounded-full bg-[#4CAF50]' />
<span className='mt-1.5 size-1 flex-shrink-0 rounded-full bg-[#4CAF50]' />
{p}
</li>
))}
@@ -274,7 +274,7 @@ export default async function PartnersPage() {
</h2>
<p className='mb-10 text-[#F6F6F0]/60 text-[18px] leading-[160%]'>
Complete Sim Academy to earn your first certification and unlock partner benefits.
It's free to start no credit card required.
It's free to start, no credit card required.
</p>
{/* TODO: Uncomment when academy is public */}
{/* <Link
+1 -1
View File
@@ -584,7 +584,7 @@ export default function PrivacyPolicy() {
Please note that we may ask you to verify your identity before responding to such
requests.
</p>
<p className='mb-4 border-[var(--landing-border-strong)] border-l-4 bg-[var(--landing-bg-elevated)] p-3 text-[var(--landing-text)]'>
<p className='mb-4 bg-[var(--landing-bg-elevated)] p-3 text-[var(--landing-text)] shadow-[inset_2px_0_0_var(--landing-border-strong)]'>
You have the right to complain to a Data Protection Authority about our collection and use
of your Personal Information. For more information, please contact your local data
protection authority in the European Economic Area (EEA).
+1 -1
View File
@@ -291,7 +291,7 @@ export default function TermsOfService() {
Agreement. The arbitration will be conducted by JAMS, an established alternative dispute
resolution provider.
</p>
<p className='mb-4 border-[var(--landing-border-strong)] border-l-4 bg-[var(--landing-bg-elevated)] p-3 text-[var(--landing-text)]'>
<p className='mb-4 bg-[var(--landing-bg-elevated)] p-3 text-[var(--landing-text)] shadow-[inset_2px_0_0_var(--landing-border-strong)]'>
YOU AND COMPANY AGREE THAT EACH OF US MAY BRING CLAIMS AGAINST THE OTHER ONLY ON AN
INDIVIDUAL BASIS AND NOT ON A CLASS, REPRESENTATIVE, OR COLLECTIVE BASIS. ONLY INDIVIDUAL
RELIEF IS AVAILABLE, AND DISPUTES OF MORE THAN ONE CUSTOMER OR USER CANNOT BE ARBITRATED

Some files were not shown because too many files have changed in this diff Show More