diff --git a/apps/docs/app/robots.txt/route.ts b/apps/docs/app/robots.txt/route.ts
index c9f62b347f8..205397a3b1c 100644
--- a/apps/docs/app/robots.txt/route.ts
+++ b/apps/docs/app/robots.txt/route.ts
@@ -10,7 +10,6 @@ export async function GET() {
User-agent: *
Disallow: /.next/
Disallow: /api/internal/
-Disallow: /_next/static/
Disallow: /admin/
Allow: /
Allow: /llms.txt
diff --git a/apps/docs/components/footer/footer.tsx b/apps/docs/components/footer/footer.tsx
index 342f0b733ab..8ed44b21d40 100644
--- a/apps/docs/components/footer/footer.tsx
+++ b/apps/docs/components/footer/footer.tsx
@@ -41,7 +41,7 @@ const RESOURCES_LINKS: FooterItem[] = [
{ label: 'Contact', href: `${SIM_SITE_URL}/contact`, external: true },
]
-/** Top model providers — mirrors the landing footer's top 8 catalog providers. */
+/** Top model providers — mirrors the landing footer's top 7 catalog providers. */
const MODEL_LINKS: FooterItem[] = [
{ label: 'All Models', href: `${SIM_SITE_URL}/models`, external: true },
{ label: 'OpenAI', href: `${SIM_SITE_URL}/models/openai`, external: true },
@@ -51,7 +51,6 @@ const MODEL_LINKS: FooterItem[] = [
{ label: 'xAI', href: `${SIM_SITE_URL}/models/xai`, external: true },
{ label: 'Cerebras', href: `${SIM_SITE_URL}/models/cerebras`, external: true },
{ label: 'Groq', href: `${SIM_SITE_URL}/models/groq`, external: true },
- { label: 'Sakana AI', href: `${SIM_SITE_URL}/models/sakana`, external: true },
]
const BLOCK_LINKS: FooterItem[] = [
@@ -84,7 +83,7 @@ const SOCIAL_LINKS: FooterItem[] = [
{ label: 'X (Twitter)', href: 'https://x.com/simdotai', external: true },
{
label: 'LinkedIn',
- href: 'https://www.linkedin.com/company/simstudioai/',
+ href: 'https://www.linkedin.com/company/simdotai/',
external: true,
},
{
diff --git a/apps/sim/app/(landing)/components/features/components/core-feature-card/core-feature-card.tsx b/apps/sim/app/(landing)/components/features/components/core-feature-card/core-feature-card.tsx
index f5a0bacbedc..f28ee54d6de 100644
--- a/apps/sim/app/(landing)/components/features/components/core-feature-card/core-feature-card.tsx
+++ b/apps/sim/app/(landing)/components/features/components/core-feature-card/core-feature-card.tsx
@@ -48,6 +48,7 @@ export function CoreFeatureCard({
const graphic = (
{
root = null
host?.remove()
host = null
+ vi.unstubAllGlobals()
})
function mount(strict = false): HTMLElement {
@@ -106,6 +107,34 @@ describe('foldScrollLeft', () => {
})
describe('FeaturesRail', () => {
+ it('waits until the rail approaches the viewport before adding the loop copies', () => {
+ let notify: IntersectionObserverCallback | undefined
+ const disconnect = vi.fn()
+ const observe = vi.fn()
+ vi.stubGlobal(
+ 'IntersectionObserver',
+ class {
+ constructor(callback: IntersectionObserverCallback) {
+ notify = callback
+ }
+ observe = observe
+ disconnect = disconnect
+ }
+ )
+ const rail = mount()
+ expect(observe).toHaveBeenCalledWith(rail)
+ expect(rail.children).toHaveLength(3)
+ const observer = {} as IntersectionObserver
+ act(() => notify?.([{ isIntersecting: false } as IntersectionObserverEntry], observer))
+ expect(rail.children).toHaveLength(3)
+ act(() => notify?.([{ isIntersecting: true } as IntersectionObserverEntry], observer))
+ expect(rail.children).toHaveLength(9)
+ expect(rail.scrollLeft).toBe(SET)
+ expect(disconnect).toHaveBeenCalledOnce()
+ act(() => notify?.([{ isIntersecting: false } as IntersectionObserverEntry], observer))
+ expect(rail.children).toHaveLength(9)
+ })
+
it('server-renders the finite rail once, with the scroll chrome', () => {
const html = renderToStaticMarkup(
{cards()}
diff --git a/apps/sim/app/(landing)/components/features/components/features-rail/features-rail.tsx b/apps/sim/app/(landing)/components/features/components/features-rail/features-rail.tsx
index c2bcf2058dc..93ef0f0bd67 100644
--- a/apps/sim/app/(landing)/components/features/components/features-rail/features-rail.tsx
+++ b/apps/sim/app/(landing)/components/features/components/features-rail/features-rail.tsx
@@ -86,8 +86,8 @@ interface FeaturesRailProps {
/** Accessible name of the scrolling region. */
label: string
/**
- * The cards, in order. Each becomes one slot; once JS runs the whole set is
- * cloned on both sides so the rail loops.
+ * The cards, in order. Each becomes one slot; as the rail approaches the
+ * viewport the whole set is cloned on both sides so the rail loops.
*/
children: ReactNode
}
@@ -96,7 +96,7 @@ interface FeaturesRailProps {
* The homepage product rail: native horizontal scrolling that never ends.
*
* The server renders the set once, so the HTML - and any visit without JS - is
- * the plain finite rail with the first card under the heading. After hydration
+ * the plain finite rail with the first card under the heading. Near the viewport
* the set is cloned once on each side, the scroll position jumps one set width
* before paint so nothing visibly moves (folded, so Strict Mode's second run of
* the effect lands on the same spot), and a passive scroll listener folds the
@@ -124,7 +124,22 @@ export function FeaturesRail({ label, children }: FeaturesRailProps) {
const cards = Children.toArray(children)
useEffect(() => {
- setLooping(true)
+ const rail = railRef.current
+ if (!rail) return
+ if (typeof IntersectionObserver === 'undefined') {
+ setLooping(true)
+ return
+ }
+ const observer = new IntersectionObserver(
+ (entries) => {
+ if (!entries.some((entry) => entry.isIntersecting)) return
+ setLooping(true)
+ observer.disconnect()
+ },
+ { rootMargin: '600px' }
+ )
+ observer.observe(rail)
+ return () => observer.disconnect()
}, [])
useLayoutEffect(() => {
diff --git a/apps/sim/app/(landing)/components/footer/footer.tsx b/apps/sim/app/(landing)/components/footer/footer.tsx
index 44be284c0d4..4d8d7fae2bc 100644
--- a/apps/sim/app/(landing)/components/footer/footer.tsx
+++ b/apps/sim/app/(landing)/components/footer/footer.tsx
@@ -87,7 +87,7 @@ const RESOURCES_LINKS: FooterItem[] = [
/** Top model providers, sourced from the catalog so labels/hrefs never drift. */
const MODEL_LINKS: FooterItem[] = [
{ label: 'All Models', href: '/models' },
- ...MODEL_PROVIDERS_WITH_CATALOGS.slice(0, 8).map((provider) => ({
+ ...MODEL_PROVIDERS_WITH_CATALOGS.slice(0, 7).map((provider) => ({
label: provider.name,
href: provider.href,
})),
@@ -119,7 +119,7 @@ const SOCIAL_LINKS: FooterItem[] = [
{ label: 'X (Twitter)', href: 'https://x.com/simdotai', external: true },
{
label: 'LinkedIn',
- href: 'https://www.linkedin.com/company/simstudioai/',
+ href: 'https://www.linkedin.com/company/simdotai/',
external: true,
},
{
diff --git a/apps/sim/app/(landing)/components/hero/components/hero-platform-loop/production-workflow-stage.tsx b/apps/sim/app/(landing)/components/hero/components/hero-platform-loop/production-workflow-stage.tsx
index b142adc61ce..54e972887d8 100644
--- a/apps/sim/app/(landing)/components/hero/components/hero-platform-loop/production-workflow-stage.tsx
+++ b/apps/sim/app/(landing)/components/hero/components/hero-platform-loop/production-workflow-stage.tsx
@@ -75,20 +75,20 @@ const EMPTY_IDS: ReadonlySet
= new Set()
const ACTION_BUTTON_STYLES = [
'size-[24px] rounded-md p-0',
'border-none bg-transparent text-[var(--text-icon)]',
- 'hover-hover:bg-[var(--surface-5)] hover-hover:!text-[var(--text-primary)]',
- 'dark:hover-hover:bg-[var(--surface-4)]',
- 'transition-[background-color,color,opacity,transform] duration-150 active:scale-[0.96]',
+ 'transition-[background-color,color,opacity,transform] duration-150',
'group-data-[node-selected]:text-[var(--surface-2)]',
- 'hover-hover:group-data-[node-selected]:bg-[var(--surface-2)]',
- 'hover-hover:group-data-[node-selected]:!text-[var(--text-primary)]',
].join(' ')
const FIRST_ACTION_STYLES =
"!w-[40px] [clip-path:path('M23.75_0A8_8_0_0_0_17.6_2.88L3.41_19.9A2.5_2.5_0_0_0_5.34_24L36_24A4_4_0_0_0_40_20L40_4A4_4_0_0_0_36_0Z')] [&>svg]:translate-x-[8px] [&>svg]:translate-y-px"
+/** A 24px target even at MIN_ZOOM, extending above/left of the unchanged 40px painted slot. */
+const RUN_ACTION_HIT_STYLES =
+ 'group/run relative -ml-[14px] size-[54px] shrink-0 border-none bg-transparent! p-0'
+
/** The running run slot: graphite fill, inverse glyph - the editor's own treatment. */
const RUNNING_RUN_STYLES =
- '!bg-[var(--text-secondary)] !text-[var(--text-inverse)] hover-hover:!bg-[var(--white)] hover-hover:!text-[var(--surface-inverted)]'
+ '!bg-[var(--text-secondary)] !text-[var(--text-inverse)] group-hover-hover/run:!bg-[var(--white)] group-hover-hover/run:!text-[var(--surface-inverted)]'
/** A bystander card's actions dim mid-run; the run/stop slot keeps its ordinary chrome. */
const BYSTANDER_ACTION_STYLES =
'!bg-transparent !opacity-25 hover-hover:!bg-transparent dark:hover-hover:!bg-transparent'
@@ -227,7 +227,7 @@ function PreviewActionBar({ block, running, workflowRunning, onRunToggle }: Prev
return (
{sweeping && (
@@ -250,35 +250,42 @@ function PreviewActionBar({ block, running, workflowRunning, onRunToggle }: Prev
)}
-
+
@@ -288,23 +295,22 @@ function PreviewActionBar({ block, running, workflowRunning, onRunToggle }: Prev
{inertActions.map(({ label, Icon }) => (
-
+
+
+
{!workflowRunning && {label}}
diff --git a/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/nav-menu-chip.test.tsx b/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/nav-menu-chip.test.tsx
index c2523b051cb..37ea2107d22 100644
--- a/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/nav-menu-chip.test.tsx
+++ b/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/nav-menu-chip.test.tsx
@@ -27,6 +27,13 @@ vi.mock('next/link', () => ({
),
}))
+vi.mock('next/dynamic', () => ({
+ default:
+ () =>
+ ({ item }: { item: NavMenuItemData }) => (
+
+ ),
+}))
vi.mock('@/app/(landing)/components/chevron-arrow', () => ({
ChevronArrow: () => null,
}))
@@ -104,6 +111,17 @@ function expectSelected(href: string, kind: string) {
}
describe('NavMenuCluster feature selection', () => {
+ it('mounts the preview on first opening and preserves it during the exit transition', () => {
+ expect(host.querySelector('output')).toBeNull()
+ hover(element('#nav-platform-menu-trigger'))
+ expect(host.querySelector('output')).not.toBeNull()
+ act(() => {
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
+ })
+ expect(element('#primary-navigation-mega-menu').getAttribute('aria-hidden')).toBe('true')
+ expect(host.querySelector('output')).not.toBeNull()
+ })
+
it('prefetches destinations only while their menu is open', () => {
const overview = element('a[href="/platform"]')
const customers = element('#nav-customers-menu a[href="/customers/rivian"]')
diff --git a/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/nav-menu-chip.tsx b/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/nav-menu-chip.tsx
index 80afd4e6fc2..ce6f699f547 100644
--- a/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/nav-menu-chip.tsx
+++ b/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/nav-menu-chip.tsx
@@ -2,6 +2,7 @@
import { type ReactNode, useEffect, useRef, useState } from 'react'
import { ChipChevronDown, chipContentLabelClass, chipVariants, cn } from '@sim/emcn'
+import dynamic from 'next/dynamic'
import { flushSync } from 'react-dom'
import {
HOME_INSET,
@@ -11,11 +12,18 @@ import {
import { NavMenuCard } from '@/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-card'
import { NavMenuItem } from '@/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-item'
import { NavMenuLogoMarquee } from '@/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-logo-marquee'
-import { NavMenuPreview } from '@/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-preview/nav-menu-preview'
import type { NavMenu } from '@/app/(landing)/components/navbar/components/nav-menu-chip/types'
import { NAVBAR_GLASS_SURFACE } from '@/app/(landing)/components/navbar/components/navbar-shell'
import { useNavbarMenu } from '@/app/(landing)/components/navbar/hooks/use-navbar-menu'
+const NavMenuPreview = dynamic(
+ () =>
+ import(
+ '@/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-preview/nav-menu-preview'
+ ).then((module) => module.NavMenuPreview),
+ { loading: () => }
+)
+
interface NavMenuClusterProps {
/** Non-empty group of mega-menus that share one stable panel. */
menus: readonly [NavMenu, ...NavMenu[]]
@@ -68,6 +76,7 @@ export function NavMenuCluster({ menus, modelsPreview }: NavMenuClusterProps) {
() => menus.find((menu) => !isFloating(menu)) ?? menus[0]
)
const [activeItem, setActiveItem] = useState(surfaceMenu.sections[0].items[0])
+ const [previewMounted, setPreviewMounted] = useState(false)
useEffect(() => {
if (!open) return
@@ -86,6 +95,7 @@ export function NavMenuCluster({ menus, modelsPreview }: NavMenuClusterProps) {
const activateMenu = (menu: NavMenu) => {
setActiveMenu(menu)
if (!isFloating(menu)) {
+ setPreviewMounted(true)
setSurfaceMenu(menu)
setActiveItem(menu.sections[0].items[0])
}
@@ -270,7 +280,7 @@ export function NavMenuCluster({ menus, modelsPreview }: NavMenuClusterProps) {
))}
-
+ {previewMounted &&
}
diff --git a/apps/sim/app/(landing)/components/site-structured-data/site-structured-data.tsx b/apps/sim/app/(landing)/components/site-structured-data/site-structured-data.tsx
index a4b8fa24914..591920d098e 100644
--- a/apps/sim/app/(landing)/components/site-structured-data/site-structured-data.tsx
+++ b/apps/sim/app/(landing)/components/site-structured-data/site-structured-data.tsx
@@ -36,7 +36,7 @@ const SITE_JSON_LD = {
sameAs: [
'https://x.com/simdotai',
'https://github.com/simstudioai/sim',
- 'https://www.linkedin.com/company/simstudioai/',
+ 'https://www.linkedin.com/company/simdotai/',
'https://join.slack.com/t/sim-ott9864/shared_invite/zt-43lp8tc5v-0qrrqHGBKUsvQlpoouH~TA',
],
contactPoint: [
diff --git a/apps/sim/app/(landing)/cookie-policy/cookie-policy-content.tsx b/apps/sim/app/(landing)/cookie-policy/cookie-policy-content.tsx
index 4da00f38e27..563b7b7f9f3 100644
--- a/apps/sim/app/(landing)/cookie-policy/cookie-policy-content.tsx
+++ b/apps/sim/app/(landing)/cookie-policy/cookie-policy-content.tsx
@@ -44,7 +44,7 @@ export const COOKIE_POLICY_CONFIG: LegalPageConfig = {
title: 'Cookie Policy',
description:
'What cookies Sim sets, why, how long they last, and how to change your choice at any time.',
- lastUpdated: 'September 3, 2026',
+ lastUpdated: 'September 17, 2026',
intro: [
{
kind: 'paragraph',
@@ -165,7 +165,7 @@ export const COOKIE_POLICY_CONFIG: LegalPageConfig = {
[
'__cf_bm',
'Cloudflare',
- 'Bot-management check on requests to providers we load, such as HubSpot and X.',
+ 'Bot-management check on requests to providers we load, such as X.',
'30 minutes',
],
]),
@@ -177,10 +177,6 @@ export const COOKIE_POLICY_CONFIG: LegalPageConfig = {
'Holds the session state for a specific Analytics property.',
'13 months',
],
- ['__hstc', 'HubSpot', 'Tracks visits across sessions for the main tracker.', '6 months'],
- ['hubspotutk', 'HubSpot', 'Identifies a visitor across form submissions.', '6 months'],
- ['__hssc', 'HubSpot', 'Tracks the current session.', '30 minutes'],
- ['__hssrc', 'HubSpot', 'Detects whether the visitor restarted their browser.', 'Session'],
[
'ph_*_posthog',
'PostHog',
@@ -269,9 +265,8 @@ export const COOKIE_POLICY_CONFIG: LegalPageConfig = {
Google Analytics
, Google Ads,{' '}
- X (Twitter),{' '}
- HubSpot, and{' '}
- PostHog.
+ X (Twitter),
+ and PostHog.
>
),
},
@@ -292,7 +287,6 @@ export const COOKIE_POLICY_CONFIG: LegalPageConfig = {
The providers currently in use are{' '}
Google{' '}
(Analytics and Ads),{' '}
- HubSpot,{' '}
X (Twitter),{' '}
Ahrefs,{' '}
PostHog, and{' '}
diff --git a/apps/sim/app/(landing)/files/components/feature-graphics/interactive-library-folder.tsx b/apps/sim/app/(landing)/files/components/feature-graphics/interactive-library-folder.tsx
index ad1d78fd5b5..cff20bc4ebd 100644
--- a/apps/sim/app/(landing)/files/components/feature-graphics/interactive-library-folder.tsx
+++ b/apps/sim/app/(landing)/files/components/feature-graphics/interactive-library-folder.tsx
@@ -84,7 +84,7 @@ export function InteractiveLibraryFolder({