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 }) => ( + {item.preview.kind} + ), +})) 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({