Skip to content

Commit 9d608e1

Browse files
committed
fix(landing): repair public links and streamline landing previews
1 parent cbdb864 commit 9d608e1

23 files changed

Lines changed: 527 additions & 343 deletions

File tree

‎apps/docs/app/robots.txt/route.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ export async function GET() {
1010
User-agent: *
1111
Disallow: /.next/
1212
Disallow: /api/internal/
13-
Disallow: /_next/static/
1413
Disallow: /admin/
1514
Allow: /
1615
Allow: /llms.txt

‎apps/docs/components/footer/footer.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ const SOCIAL_LINKS: FooterItem[] = [
8484
{ label: 'X (Twitter)', href: 'https://x.com/simdotai', external: true },
8585
{
8686
label: 'LinkedIn',
87-
href: 'https://www.linkedin.com/company/simstudioai/',
87+
href: 'https://www.linkedin.com/company/simdotai/',
8888
external: true,
8989
},
9090
{

‎apps/sim/app/(landing)/components/features/components/core-feature-card/core-feature-card.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export function CoreFeatureCard({
4848
const graphic = (
4949
<div
5050
aria-hidden={interactiveVisual ? undefined : true}
51+
inert={!interactiveVisual}
5152
className={cn(
5253
'relative aspect-[5/6] overflow-hidden border border-[var(--border)] transition-colors duration-300 group-hover:border-[var(--border)] motion-reduce:transition-none',
5354
LANDING_STAGE_RADIUS,

‎apps/sim/app/(landing)/components/features/components/features-rail/features-rail.test.tsx‎

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import { act, StrictMode } from 'react'
55
import { createRoot, type Root } from 'react-dom/client'
66
import { renderToStaticMarkup } from 'react-dom/server'
7-
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest'
7+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
88
import {
99
FeaturesRail,
1010
foldScrollLeft,
@@ -74,6 +74,7 @@ afterEach(() => {
7474
root = null
7575
host?.remove()
7676
host = null
77+
vi.unstubAllGlobals()
7778
})
7879

7980
function mount(strict = false): HTMLElement {
@@ -106,6 +107,34 @@ describe('foldScrollLeft', () => {
106107
})
107108

108109
describe('FeaturesRail', () => {
110+
it('waits until the rail approaches the viewport before adding the loop copies', () => {
111+
let notify: IntersectionObserverCallback | undefined
112+
const disconnect = vi.fn()
113+
const observe = vi.fn()
114+
vi.stubGlobal(
115+
'IntersectionObserver',
116+
class {
117+
constructor(callback: IntersectionObserverCallback) {
118+
notify = callback
119+
}
120+
observe = observe
121+
disconnect = disconnect
122+
}
123+
)
124+
const rail = mount()
125+
expect(observe).toHaveBeenCalledWith(rail)
126+
expect(rail.children).toHaveLength(3)
127+
const observer = {} as IntersectionObserver
128+
act(() => notify?.([{ isIntersecting: false } as IntersectionObserverEntry], observer))
129+
expect(rail.children).toHaveLength(3)
130+
act(() => notify?.([{ isIntersecting: true } as IntersectionObserverEntry], observer))
131+
expect(rail.children).toHaveLength(9)
132+
expect(rail.scrollLeft).toBe(SET)
133+
expect(disconnect).toHaveBeenCalledOnce()
134+
act(() => notify?.([{ isIntersecting: false } as IntersectionObserverEntry], observer))
135+
expect(rail.children).toHaveLength(9)
136+
})
137+
109138
it('server-renders the finite rail once, with the scroll chrome', () => {
110139
const html = renderToStaticMarkup(
111140
<FeaturesRail label='Core Sim features'>{cards()}</FeaturesRail>

‎apps/sim/app/(landing)/components/features/components/features-rail/features-rail.tsx‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,8 @@ interface FeaturesRailProps {
8686
/** Accessible name of the scrolling region. */
8787
label: string
8888
/**
89-
* The cards, in order. Each becomes one slot; once JS runs the whole set is
90-
* cloned on both sides so the rail loops.
89+
* The cards, in order. Each becomes one slot; as the rail approaches the
90+
* viewport the whole set is cloned on both sides so the rail loops.
9191
*/
9292
children: ReactNode
9393
}
@@ -96,7 +96,7 @@ interface FeaturesRailProps {
9696
* The homepage product rail: native horizontal scrolling that never ends.
9797
*
9898
* The server renders the set once, so the HTML - and any visit without JS - is
99-
* the plain finite rail with the first card under the heading. After hydration
99+
* the plain finite rail with the first card under the heading. Near the viewport
100100
* the set is cloned once on each side, the scroll position jumps one set width
101101
* before paint so nothing visibly moves (folded, so Strict Mode's second run of
102102
* the effect lands on the same spot), and a passive scroll listener folds the
@@ -124,7 +124,22 @@ export function FeaturesRail({ label, children }: FeaturesRailProps) {
124124
const cards = Children.toArray(children)
125125

126126
useEffect(() => {
127-
setLooping(true)
127+
const rail = railRef.current
128+
if (!rail) return
129+
if (typeof IntersectionObserver === 'undefined') {
130+
setLooping(true)
131+
return
132+
}
133+
const observer = new IntersectionObserver(
134+
(entries) => {
135+
if (!entries.some((entry) => entry.isIntersecting)) return
136+
setLooping(true)
137+
observer.disconnect()
138+
},
139+
{ rootMargin: '600px' }
140+
)
141+
observer.observe(rail)
142+
return () => observer.disconnect()
128143
}, [])
129144

130145
useLayoutEffect(() => {

‎apps/sim/app/(landing)/components/footer/footer.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ const SOCIAL_LINKS: FooterItem[] = [
119119
{ label: 'X (Twitter)', href: 'https://x.com/simdotai', external: true },
120120
{
121121
label: 'LinkedIn',
122-
href: 'https://www.linkedin.com/company/simstudioai/',
122+
href: 'https://www.linkedin.com/company/simdotai/',
123123
external: true,
124124
},
125125
{

‎apps/sim/app/(landing)/components/hero/components/hero-platform-loop/production-workflow-stage.tsx‎

Lines changed: 35 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -75,20 +75,20 @@ const EMPTY_IDS: ReadonlySet<string> = new Set()
7575
const ACTION_BUTTON_STYLES = [
7676
'size-[24px] rounded-md p-0',
7777
'border-none bg-transparent text-[var(--text-icon)]',
78-
'hover-hover:bg-[var(--surface-5)] hover-hover:!text-[var(--text-primary)]',
79-
'dark:hover-hover:bg-[var(--surface-4)]',
80-
'transition-[background-color,color,opacity,transform] duration-150 active:scale-[0.96]',
78+
'transition-[background-color,color,opacity,transform] duration-150',
8179
'group-data-[node-selected]:text-[var(--surface-2)]',
82-
'hover-hover:group-data-[node-selected]:bg-[var(--surface-2)]',
83-
'hover-hover:group-data-[node-selected]:!text-[var(--text-primary)]',
8480
].join(' ')
8581

8682
const FIRST_ACTION_STYLES =
8783
"!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"
8884

85+
/** A 24px target even at MIN_ZOOM, extending above/left of the unchanged 40px painted slot. */
86+
const RUN_ACTION_HIT_STYLES =
87+
'group/run relative -ml-[14px] size-[54px] shrink-0 border-none bg-transparent! p-0'
88+
8989
/** The running run slot: graphite fill, inverse glyph - the editor's own treatment. */
9090
const RUNNING_RUN_STYLES =
91-
'!bg-[var(--text-secondary)] !text-[var(--text-inverse)] hover-hover:!bg-[var(--white)] hover-hover:!text-[var(--surface-inverted)]'
91+
'!bg-[var(--text-secondary)] !text-[var(--text-inverse)] group-hover-hover/run:!bg-[var(--white)] group-hover-hover/run:!text-[var(--surface-inverted)]'
9292
/** A bystander card's actions dim mid-run; the run/stop slot keeps its ordinary chrome. */
9393
const BYSTANDER_ACTION_STYLES =
9494
'!bg-transparent !opacity-25 hover-hover:!bg-transparent dark:hover-hover:!bg-transparent'
@@ -227,7 +227,7 @@ function PreviewActionBar({ block, running, workflowRunning, onRunToggle }: Prev
227227
return (
228228
<div
229229
data-workflow-action-bar-swell=''
230-
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'
230+
className='-top-[28px] pointer-events-auto absolute right-[24px] z-[40] h-[28px] w-fit rounded-lg px-[0.2rem] py-0.5'
231231
>
232232
<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'>
233233
{sweeping && (
@@ -250,35 +250,42 @@ function PreviewActionBar({ block, running, workflowRunning, onRunToggle }: Prev
250250
)}
251251
<Tooltip.Root preferAbove>
252252
<Tooltip.Trigger asChild>
253-
<span className='inline-flex'>
253+
<span className='inline-flex h-full items-end'>
254254
<Button
255255
type='button'
256256
variant='ghost'
257257
aria-label={workflowRunning ? 'Stop workflow' : `Run ${block.name}`}
258-
className={cn(
259-
ACTION_BUTTON_STYLES,
260-
FIRST_ACTION_STYLES,
261-
running && RUNNING_RUN_STYLES,
262-
workflowRunning && 'group/run'
263-
)}
258+
className={RUN_ACTION_HIT_STYLES}
264259
onClick={(event) => {
265260
event.stopPropagation()
266261
onRunToggle()
267262
}}
268263
>
269-
{workflowRunning ? (
270-
running ? (
271-
<RunningActionIcon />
264+
<span
265+
className={cn(
266+
'pointer-events-none absolute right-0 bottom-0 flex items-center justify-center',
267+
ACTION_BUTTON_STYLES,
268+
FIRST_ACTION_STYLES,
269+
'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)]',
270+
'group-hover-hover/run:group-data-[node-selected]:!text-[var(--text-primary)] group-hover-hover/run:group-data-[node-selected]:bg-[var(--surface-2)]',
271+
'group-active/run:scale-[0.96]',
272+
running && RUNNING_RUN_STYLES
273+
)}
274+
>
275+
{workflowRunning ? (
276+
running ? (
277+
<RunningActionIcon />
278+
) : (
279+
<Square
280+
className='size-[11px] fill-current'
281+
aria-hidden='true'
282+
strokeWidth={0}
283+
/>
284+
)
272285
) : (
273-
<Square
274-
className='size-[11px] fill-current'
275-
aria-hidden='true'
276-
strokeWidth={0}
277-
/>
278-
)
279-
) : (
280-
<PlayOutline className='size-[14px]' />
281-
)}
286+
<PlayOutline className='size-[14px]' />
287+
)}
288+
</span>
282289
</Button>
283290
</span>
284291
</Tooltip.Trigger>
@@ -291,17 +298,14 @@ function PreviewActionBar({ block, running, workflowRunning, onRunToggle }: Prev
291298
<Button
292299
type='button'
293300
variant='ghost'
294-
aria-label={label}
301+
disabled
302+
aria-label={`${label} unavailable in preview`}
295303
className={cn(
296304
ACTION_BUTTON_STYLES,
297305
label === 'Delete' && LAST_ACTION_STYLES,
298306
workflowRunning && !running && BYSTANDER_ACTION_STYLES,
299307
sweeping && SWEEP_SLOT_STYLES
300308
)}
301-
onClick={(event) => {
302-
event.preventDefault()
303-
event.stopPropagation()
304-
}}
305309
>
306310
<Icon className='size-[14px]' />
307311
</Button>

‎apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/nav-menu-chip.test.tsx‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ vi.mock('next/link', () => ({
2727
<a {...props} data-prefetch={prefetch === false ? 'disabled' : 'auto'} />
2828
),
2929
}))
30+
vi.mock('next/dynamic', () => ({
31+
default:
32+
() =>
33+
({ item }: { item: NavMenuItemData }) => (
34+
<output aria-label='Feature preview'>{item.preview.kind}</output>
35+
),
36+
}))
3037
vi.mock('@/app/(landing)/components/chevron-arrow', () => ({
3138
ChevronArrow: () => null,
3239
}))
@@ -104,6 +111,17 @@ function expectSelected(href: string, kind: string) {
104111
}
105112

106113
describe('NavMenuCluster feature selection', () => {
114+
it('mounts the preview on first opening and preserves it during the exit transition', () => {
115+
expect(host.querySelector('output')).toBeNull()
116+
hover(element('#nav-platform-menu-trigger'))
117+
expect(host.querySelector('output')).not.toBeNull()
118+
act(() => {
119+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
120+
})
121+
expect(element('#primary-navigation-mega-menu').getAttribute('aria-hidden')).toBe('true')
122+
expect(host.querySelector('output')).not.toBeNull()
123+
})
124+
107125
it('prefetches destinations only while their menu is open', () => {
108126
const overview = element('a[href="/platform"]')
109127
const customers = element('#nav-customers-menu a[href="/customers/rivian"]')

‎apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/nav-menu-chip.tsx‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { type ReactNode, useEffect, useRef, useState } from 'react'
44
import { ChipChevronDown, chipContentLabelClass, chipVariants, cn } from '@sim/emcn'
5+
import dynamic from 'next/dynamic'
56
import { flushSync } from 'react-dom'
67
import {
78
HOME_INSET,
@@ -11,11 +12,18 @@ import {
1112
import { NavMenuCard } from '@/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-card'
1213
import { NavMenuItem } from '@/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-item'
1314
import { NavMenuLogoMarquee } from '@/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-logo-marquee'
14-
import { NavMenuPreview } from '@/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-preview/nav-menu-preview'
1515
import type { NavMenu } from '@/app/(landing)/components/navbar/components/nav-menu-chip/types'
1616
import { NAVBAR_GLASS_SURFACE } from '@/app/(landing)/components/navbar/components/navbar-shell'
1717
import { useNavbarMenu } from '@/app/(landing)/components/navbar/hooks/use-navbar-menu'
1818

19+
const NavMenuPreview = dynamic(
20+
() =>
21+
import(
22+
'@/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-preview/nav-menu-preview'
23+
).then((module) => module.NavMenuPreview),
24+
{ loading: () => <div className='min-h-[340px]' /> }
25+
)
26+
1927
interface NavMenuClusterProps {
2028
/** Non-empty group of mega-menus that share one stable panel. */
2129
menus: readonly [NavMenu, ...NavMenu[]]
@@ -68,6 +76,7 @@ export function NavMenuCluster({ menus, modelsPreview }: NavMenuClusterProps) {
6876
() => menus.find((menu) => !isFloating(menu)) ?? menus[0]
6977
)
7078
const [activeItem, setActiveItem] = useState(surfaceMenu.sections[0].items[0])
79+
const [previewMounted, setPreviewMounted] = useState(false)
7180

7281
useEffect(() => {
7382
if (!open) return
@@ -86,6 +95,7 @@ export function NavMenuCluster({ menus, modelsPreview }: NavMenuClusterProps) {
8695
const activateMenu = (menu: NavMenu) => {
8796
setActiveMenu(menu)
8897
if (!isFloating(menu)) {
98+
setPreviewMounted(true)
8999
setSurfaceMenu(menu)
90100
setActiveItem(menu.sections[0].items[0])
91101
}
@@ -270,7 +280,7 @@ export function NavMenuCluster({ menus, modelsPreview }: NavMenuClusterProps) {
270280
))}
271281
</div>
272282

273-
<NavMenuPreview item={activeItem} modelsPreview={modelsPreview} />
283+
{previewMounted && <NavMenuPreview item={activeItem} modelsPreview={modelsPreview} />}
274284
</div>
275285
</div>
276286
</div>

‎apps/sim/app/(landing)/components/site-structured-data/site-structured-data.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const SITE_JSON_LD = {
3636
sameAs: [
3737
'https://x.com/simdotai',
3838
'https://github.com/simstudioai/sim',
39-
'https://www.linkedin.com/company/simstudioai/',
39+
'https://www.linkedin.com/company/simdotai/',
4040
'https://join.slack.com/t/sim-ott9864/shared_invite/zt-43lp8tc5v-0qrrqHGBKUsvQlpoouH~TA',
4141
],
4242
contactPoint: [

0 commit comments

Comments
 (0)