Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/docs/app/robots.txt/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export async function GET() {
User-agent: *
Disallow: /.next/
Disallow: /api/internal/
Disallow: /_next/static/
Disallow: /admin/
Allow: /
Allow: /llms.txt
Expand Down
5 changes: 2 additions & 3 deletions apps/docs/components/footer/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand All @@ -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[] = [
Expand Down Expand Up @@ -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,
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export function CoreFeatureCard({
const graphic = (
<div
aria-hidden={interactiveVisual ? undefined : true}
inert={!interactiveVisual}
Comment thread
waleedlatif1 marked this conversation as resolved.
className={cn(
'relative aspect-[5/6] overflow-hidden border border-[var(--border)] transition-colors duration-300 group-hover:border-[var(--border)] motion-reduce:transition-none',
LANDING_STAGE_RADIUS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { act, StrictMode } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { renderToStaticMarkup } from 'react-dom/server'
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest'
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import {
FeaturesRail,
foldScrollLeft,
Expand Down Expand Up @@ -74,6 +74,7 @@ afterEach(() => {
root = null
host?.remove()
host = null
vi.unstubAllGlobals()
})

function mount(strict = false): HTMLElement {
Expand Down Expand Up @@ -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(
<FeaturesRail label='Core Sim features'>{cards()}</FeaturesRail>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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(() => {
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/app/(landing)/components/footer/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})),
Expand Down Expand Up @@ -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,
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,20 +75,20 @@ const EMPTY_IDS: ReadonlySet<string> = 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'
Expand Down Expand Up @@ -227,7 +227,7 @@ function PreviewActionBar({ block, running, workflowRunning, onRunToggle }: Prev
return (
<div
data-workflow-action-bar-swell=''
className='-top-[28px] pointer-events-auto absolute right-[24px] z-[40] h-[28px] w-fit overflow-hidden rounded-lg px-[0.2rem] py-0.5'
className='-top-[28px] pointer-events-auto absolute right-[24px] z-[40] h-[28px] w-fit rounded-lg px-[0.2rem] py-0.5'
>
<div className='pointer-events-none relative flex h-full flex-row items-center gap-[2px] opacity-0 transition-opacity duration-[30ms] [transition-timing-function:cubic-bezier(0.23,1,0.32,1)] group-data-[action-menu-ready]:pointer-events-auto group-data-[action-menu-ready]:opacity-100 group-data-[action-menu-ready]:duration-100'>
{sweeping && (
Expand All @@ -250,35 +250,42 @@ function PreviewActionBar({ block, running, workflowRunning, onRunToggle }: Prev
)}
<Tooltip.Root preferAbove>
<Tooltip.Trigger asChild>
<span className='inline-flex'>
<span className='inline-flex h-full items-end'>
<Button
type='button'
variant='ghost'
aria-label={workflowRunning ? 'Stop workflow' : `Run ${block.name}`}
className={cn(
ACTION_BUTTON_STYLES,
FIRST_ACTION_STYLES,
running && RUNNING_RUN_STYLES,
workflowRunning && 'group/run'
)}
className={RUN_ACTION_HIT_STYLES}
onClick={(event) => {
event.stopPropagation()
onRunToggle()
}}
>
{workflowRunning ? (
running ? (
<RunningActionIcon />
<span
className={cn(
'pointer-events-none absolute right-0 bottom-0 flex items-center justify-center',
ACTION_BUTTON_STYLES,
FIRST_ACTION_STYLES,
'group-hover-hover/run:!text-[var(--text-primary)] group-hover-hover/run:bg-[var(--surface-5)] dark:group-hover-hover/run:bg-[var(--surface-4)]',
'group-hover-hover/run:group-data-[node-selected]:!text-[var(--text-primary)] group-hover-hover/run:group-data-[node-selected]:bg-[var(--surface-2)]',
'group-active/run:scale-[0.96]',
running && RUNNING_RUN_STYLES
)}
>
{workflowRunning ? (
running ? (
<RunningActionIcon />
) : (
<Square
className='size-[11px] fill-current'
aria-hidden='true'
strokeWidth={0}
/>
)
) : (
<Square
className='size-[11px] fill-current'
aria-hidden='true'
strokeWidth={0}
/>
)
) : (
<PlayOutline className='size-[14px]' />
)}
<PlayOutline className='size-[14px]' />
)}
</span>
</Button>
</span>
</Tooltip.Trigger>
Expand All @@ -288,23 +295,22 @@ function PreviewActionBar({ block, running, workflowRunning, onRunToggle }: Prev
{inertActions.map(({ label, Icon }) => (
<Tooltip.Root key={label} preferAbove>
<Tooltip.Trigger asChild>
<Button
type='button'
variant='ghost'
aria-label={label}
className={cn(
ACTION_BUTTON_STYLES,
label === 'Delete' && LAST_ACTION_STYLES,
workflowRunning && !running && BYSTANDER_ACTION_STYLES,
sweeping && SWEEP_SLOT_STYLES
)}
onClick={(event) => {
event.preventDefault()
event.stopPropagation()
}}
>
<Icon className='size-[14px]' />
</Button>
<span className='inline-flex'>
<Button
type='button'
variant='ghost'
disabled
aria-label={`${label} unavailable in preview`}
className={cn(
ACTION_BUTTON_STYLES,
label === 'Delete' && LAST_ACTION_STYLES,
workflowRunning && !running && BYSTANDER_ACTION_STYLES,
sweeping && SWEEP_SLOT_STYLES
)}
>
<Icon className='size-[14px]' />
</Button>
</span>
</Tooltip.Trigger>
{!workflowRunning && <Tooltip.Content side='top'>{label}</Tooltip.Content>}
</Tooltip.Root>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ vi.mock('next/link', () => ({
<a {...props} data-prefetch={prefetch === false ? 'disabled' : 'auto'} />
),
}))
vi.mock('next/dynamic', () => ({
default:
() =>
({ item }: { item: NavMenuItemData }) => (
<output aria-label='Feature preview'>{item.preview.kind}</output>
),
}))
vi.mock('@/app/(landing)/components/chevron-arrow', () => ({
ChevronArrow: () => null,
}))
Expand Down Expand Up @@ -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"]')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Comment thread
waleedlatif1 marked this conversation as resolved.
import {
HOME_INSET,
Expand All @@ -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: () => <div className='min-h-[340px]' /> }
)

interface NavMenuClusterProps {
/** Non-empty group of mega-menus that share one stable panel. */
menus: readonly [NavMenu, ...NavMenu[]]
Expand Down Expand Up @@ -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
Expand All @@ -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])
}
Expand Down Expand Up @@ -270,7 +280,7 @@ export function NavMenuCluster({ menus, modelsPreview }: NavMenuClusterProps) {
))}
</div>

<NavMenuPreview item={activeItem} modelsPreview={modelsPreview} />
{previewMounted && <NavMenuPreview item={activeItem} modelsPreview={modelsPreview} />}
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
Loading
Loading