Skip to content

Commit 2c2c210

Browse files
committed
feat(landing): deliver phase one marketing redesign
1 parent be408ee commit 2c2c210

272 files changed

Lines changed: 16798 additions & 1992 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/** @vitest-environment node */
2+
import type { ReactNode } from 'react'
3+
import { renderToStaticMarkup } from 'react-dom/server'
4+
import { expect, it, vi } from 'vitest'
5+
import { AuthShell } from '@/app/(auth)/components/auth-shell'
6+
7+
vi.mock('next/link', () => ({
8+
default: ({ children }: { children: ReactNode }) => (
9+
<a href='/' data-client-navigation>
10+
{children}
11+
</a>
12+
),
13+
}))
14+
vi.mock('@/app/_shell/desktop-title-bar', () => ({ DesktopTitleBarLane: () => null }))
15+
vi.mock('@/app/(landing)/components/navbar/components', () => ({
16+
LogoMark: ({ children }: { children: ReactNode }) => <>{children}</>,
17+
SimWordmark: () => 'Sim',
18+
}))
19+
20+
it('returns home through a document link so route-specific theme defaults reinitialize', () => {
21+
const html = renderToStaticMarkup(<AuthShell>Sign in</AuthShell>)
22+
expect(html).toContain('href="/" aria-label="Sim home"')
23+
expect(html).not.toContain('data-client-navigation')
24+
})

apps/sim/app/(auth)/components/auth-shell.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { ReactNode } from 'react'
2-
import Link from 'next/link'
32
import { DesktopTitleBarLane } from '@/app/_shell/desktop-title-bar'
43
import { LogoMark, SimWordmark } from '@/app/(landing)/components/navbar/components'
54

@@ -19,6 +18,8 @@ interface AuthShellProps {
1918
* the canvas/`--text-primary` surface, and renders a logo-only header that reuses
2019
* the landing {@link LogoMark} + {@link SimWordmark} at the same nav gutters. The
2120
* single content column is centered and capped for a calm single-form layout.
21+
* The home link starts a document navigation so the marketing theme default is
22+
* initialized independently of auth's forced-light context.
2223
*
2324
* The shell also owns the macOS traffic-light lane, unconditionally — every surface that
2425
* wears it (the `(auth)` routes, the CLI auth handoff, the invite pages) sits outside
@@ -34,11 +35,11 @@ export function AuthShell({ children, footer }: AuthShellProps) {
3435
<DesktopTitleBarLane />
3536
<header>
3637
<nav className='mx-auto flex w-full max-w-[1446px] items-center px-12 py-4 max-sm:px-5 max-lg:px-8'>
37-
<Link href='/' aria-label='Sim home' className='flex h-[30px] items-center'>
38+
<a href='/' aria-label='Sim home' className='flex h-[30px] items-center'>
3839
<LogoMark>
3940
<SimWordmark />
4041
</LogoMark>
41-
</Link>
42+
</a>
4243
</nav>
4344
</header>
4445
<div className='flex flex-1 items-center justify-center px-4 pb-16'>
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/** @vitest-environment jsdom */
2+
import { act, type InputHTMLAttributes, type ReactNode } from 'react'
3+
import { createRoot, type Root } from 'react-dom/client'
4+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5+
import SignupForm from '@/app/(auth)/signup/signup-form'
6+
7+
const { push, signUp, refetchSession } = vi.hoisted(() => ({
8+
push: vi.fn(),
9+
signUp: vi.fn(),
10+
refetchSession: vi.fn(),
11+
}))
12+
13+
vi.mock('next/navigation', () => ({
14+
useRouter: () => ({ push }),
15+
useSearchParams: () => new URLSearchParams(),
16+
}))
17+
vi.mock('@marsidev/react-turnstile', () => ({ Turnstile: () => null }))
18+
vi.mock('posthog-js/react', () => ({ usePostHog: () => null }))
19+
vi.mock('@/lib/analytics/google', () => ({ trackGoogleEvent: vi.fn() }))
20+
vi.mock('@/lib/auth/auth-client', () => ({
21+
client: { signUp: { email: signUp } },
22+
useSession: () => ({ refetch: refetchSession }),
23+
}))
24+
vi.mock('@/lib/consent/tracking-consent', () => ({
25+
useTrackingConsent: () => ({ measurement: false }),
26+
}))
27+
vi.mock('@/lib/core/config/env', () => ({ getEnv: () => undefined, isFalsy: () => false }))
28+
vi.mock('@/lib/core/config/env-flags', () => ({ isSsoEnabled: false }))
29+
vi.mock('@/lib/core/security/input-validation', () => ({ validateCallbackUrl: () => false }))
30+
vi.mock('@/lib/messaging/email/validation', () => ({
31+
quickValidateEmail: () => ({ isValid: true }),
32+
}))
33+
vi.mock('@/lib/posthog/client', () => ({ captureClientEvent: vi.fn(), captureEvent: vi.fn() }))
34+
vi.mock('@/app/(auth)/components', () => ({
35+
AuthDivider: () => null,
36+
AuthField: ({ children }: { children: ReactNode }) => <>{children}</>,
37+
AuthFormMessage: () => null,
38+
AuthHeader: () => null,
39+
AuthInput: ({ error, ...props }: InputHTMLAttributes<HTMLInputElement> & { error?: boolean }) => (
40+
<input {...props} />
41+
),
42+
AuthLegalFooter: () => null,
43+
AuthNavPrompt: () => null,
44+
AuthSubmitButton: ({ children }: { children: ReactNode }) => (
45+
<button type='submit'>{children}</button>
46+
),
47+
PasswordInput: ({
48+
error,
49+
...props
50+
}: InputHTMLAttributes<HTMLInputElement> & { error?: boolean }) => <input {...props} />,
51+
SocialLoginButtons: () => null,
52+
SSOLoginButton: () => null,
53+
}))
54+
55+
let root: Root
56+
let host: HTMLDivElement
57+
let destination: { href: string }
58+
59+
beforeEach(() => {
60+
vi.clearAllMocks()
61+
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
62+
destination = { href: '' }
63+
const browser = window
64+
vi.stubGlobal(
65+
'window',
66+
new Proxy(browser, {
67+
get(target, key) {
68+
return key === 'location' ? destination : Reflect.get(target, key, target)
69+
},
70+
})
71+
)
72+
signUp.mockResolvedValue({ data: { user: { id: 'new-user' } } })
73+
refetchSession.mockResolvedValue(undefined)
74+
host = document.createElement('div')
75+
document.body.append(host)
76+
root = createRoot(host)
77+
})
78+
79+
afterEach(() => {
80+
act(() => root.unmount())
81+
host.remove()
82+
vi.unstubAllGlobals()
83+
})
84+
85+
async function submit(emailVerificationEnabled: boolean) {
86+
act(() =>
87+
root.render(
88+
<SignupForm
89+
githubAvailable={false}
90+
googleAvailable={false}
91+
microsoftAvailable={false}
92+
emailSignupEnabled
93+
emailVerificationEnabled={emailVerificationEnabled}
94+
/>
95+
)
96+
)
97+
const fields = { name: 'Test Builder', email: 'builder@example.com', password: 'SafePass1!' }
98+
for (const [name, value] of Object.entries(fields)) {
99+
const input = host.querySelector<HTMLInputElement>(`input[name="${name}"]`)
100+
if (!input) throw new Error(`Missing ${name} input`)
101+
input.value = value
102+
}
103+
const form = host.querySelector('form')
104+
if (!form) throw new Error('Missing signup form')
105+
await act(async () =>
106+
form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
107+
)
108+
}
109+
110+
describe('signup shell navigation', () => {
111+
it('starts a document navigation after a successful signup without verification', async () => {
112+
await submit(false)
113+
expect(signUp).toHaveBeenCalledOnce()
114+
expect(refetchSession).toHaveBeenCalledOnce()
115+
expect(destination.href).toBe('/workspace')
116+
expect(push).not.toHaveBeenCalled()
117+
})
118+
119+
it('keeps verification within the auth shell and stores the email for the next step', async () => {
120+
await submit(true)
121+
expect(push).toHaveBeenCalledWith('/verify?fromSignup=true')
122+
expect(sessionStorage.getItem('verificationEmail')).toBe('builder@example.com')
123+
expect(destination.href).toBe('')
124+
})
125+
126+
it('does not navigate when signup fails', async () => {
127+
signUp.mockResolvedValue({ error: { message: 'Signup failed' } })
128+
await submit(false)
129+
expect(push).not.toHaveBeenCalled()
130+
expect(destination.href).toBe('')
131+
})
132+
})

apps/sim/app/(auth)/signup/signup-form.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -372,12 +372,10 @@ function SignupFormContent({
372372

373373
if (destination.kind === 'verify') {
374374
router.push(VERIFY_FROM_SIGNUP_ROUTE)
375-
} else if (destination.kind === 'redirect') {
376-
// Full navigation, matching the verify hop: the destination (invite, CLI
377-
// handoff) is server-rendered and must see the fresh session cookie.
378-
window.location.href = destination.url
379375
} else {
380-
router.push(DEFAULT_POST_AUTH_ROUTE)
376+
/** Match login/verification: refresh session-bound shells and their theme default. */
377+
window.location.href =
378+
destination.kind === 'redirect' ? destination.url : DEFAULT_POST_AUTH_ROUTE
381379
}
382380
} catch (error) {
383381
logger.error('Signup error:', error)

apps/sim/app/(landing)/CLAUDE.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ This route group owns `/` and the entire public marketing surface - the home pag
66

77
## What this is
88

9-
- `app/(landing)/` - the marketing site. A shared `layout.tsx` renders the chrome once (the `LandingShell`: light tokens, navbar with server-side GitHub stars, footer, site-wide JSON-LD); each page supplies only its `<main>` content.
9+
- `app/(landing)/` - the marketing site. A shared `layout.tsx` renders the chrome once (the `LandingShell`: light tokens, navbar with server-side GitHub stars, painted pre-footer CTA, footer, site-wide JSON-LD); each page supplies only its `<main>` content. The painted light/dark CTA and footer are owned by `LandingShell`; never add page-specific closing CTA bands or footer instances.
1010
- The legacy `app/(home)/` group (old dark landing + `--landing-*` tokens) has been **deleted** - its marketing pages were migrated here and its chrome retired. Do not reintroduce `--landing-*` tokens, Martian Mono accents, or a separate marketing theme.
1111

12-
## Styling - draw from the platform's light mode
12+
## Styling - draw from the platform's tokens
1313

14-
The landing page looks like the product. Its visual language is the workspace UI in light mode, not a separate marketing theme.
14+
The landing page looks like the product. Its visual language is the workspace UI - light by default, dark on request - not a separate marketing theme.
1515

16-
- **Always light.** The root wrapper in `landing.tsx` carries the `light` class, which pins every token to its light value (see `app/_styles/globals.css`, the `:root, .light` block). Never add `dark:` variants here; never read the user's theme.
16+
- **Light by default, dark on request.** The landing family follows the theme class on `<html>` (next-themes, storage key `sim-theme`): a first-time visitor gets light, the design baseline, and the footer's `ThemeToggle` switches to dark - the platform's own `.dark` token values from `app/_styles/globals.css`, no separate palette. Tokens flip on their own, so `dark:` variants exist here only to pair the handful of deliberate literals (the `#F8F8F8` paper band, the composer send button, the pale CTA drawing) with their dark value in the same class string - never leave a literal unpaired. Never read the theme in a Server Component; the toggle is the one client reader.
1717
- **Use platform tokens, never hex.** Canvas `--bg`, surfaces `--surface-1``--surface-7`, cards/modals `--surface-2`, hover `--surface-hover`, active `--surface-active`; text `--text-primary` / `--text-secondary` / `--text-muted` / `--text-body`, icons `--text-icon`; borders `--border` (dividers) / `--border-1` (fields); brand `--brand-agent` / `--brand-secondary` / `--brand-accent`. Do **not** use the legacy `--landing-*` tokens - they belong to the old dark landing.
1818
- **Use emcn components where they fit.** The chip family (`Chip`, `ChipLink`, `ChipTag`, `ChipInput`, `ChipModal*`, …) from `@/components/emcn` is the canonical chrome - a demo-request form is a `ChipModal` with `ChipModalField`s, a pill CTA is a `Chip`/`ChipLink`. Components own their chrome; pass props, not className overrides. Full consumer rules: `.claude/rules/sim-styling.md`.
1919
- **Typography is the platform's.** Season is the global body font (`font-season` is applied on `<body>` in the root layout). Use the platform text scale (`text-small` = 13px, `text-base` = 15px, etc. - see the `@theme` block in `app/_styles/globals.css`). Don't add new fonts or font CSS variables without explicit direction.
@@ -99,7 +99,7 @@ Absolute imports only in component code (`@/app/(landing)/components/...`); `ind
9999

100100
1. Server Component unless it provably needs client state; if client, it's a leaf.
101101
2. H2 with `id` + `aria-labelledby` wiring; heading hierarchy intact.
102-
3. Platform light tokens and emcn chrome only - no hex colors, no `--landing-*`, no `dark:`.
102+
3. Platform tokens and emcn chrome only - no hex colors, no `--landing-*`. Check the section in dark mode too (footer toggle); any deliberate literal carries its `dark:` pair.
103103
4. Images: `next/image`, explicit dimensions, `priority` only on the LCP element.
104104
5. Copy passes the constitution (language table, claim hierarchy, tone).
105105
6. "Sim" named explicitly; section quotable in isolation.

apps/sim/app/(landing)/careers/careers.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ interface CareersProps {
2121
* crawlable HTML; the interactive {@link JobBoard} hydrates on top to add
2222
* Team/Location filtering.
2323
*
24-
* Both sections share the landing gutter — capped and centered at `max-w-[1460px]`
25-
* with the navbar-aligned `px-20 max-lg:px-8 max-sm:px-5` so the headline starts on
24+
* Both sections share the landing gutter — capped and centered at `max-w-[1728px]`
25+
* with the navbar-aligned `px-10 max-md:px-7 max-lg:px-8 max-xl:px-9` so the headline starts on
2626
* the same vertical line as the wordmark. The hero carries the single `<h1>`
2727
* (containing "Sim" and "AI workspace") plus an sr-only product summary for AI
2828
* citation (landing CLAUDE.md → GEO); the roles section owns its own `<h2>`.
@@ -45,7 +45,7 @@ export default async function Careers({ searchParams }: CareersProps) {
4545
<section
4646
id='careers-hero'
4747
aria-labelledby='careers-heading'
48-
className='mx-auto flex w-full max-w-[1460px] flex-col gap-5 px-20 pt-20 pb-10 max-sm:px-5 max-sm:pt-16 max-lg:px-8'
48+
className='mx-auto flex w-full max-w-[1728px] flex-col gap-5 px-10 pt-20 pb-10 max-sm:pt-16 max-md:px-7 max-lg:px-8 max-xl:px-9'
4949
>
5050
<p className='sr-only'>
5151
Careers at Sim, the open-source AI workspace where teams build, deploy, and manage AI
@@ -70,7 +70,7 @@ export default async function Careers({ searchParams }: CareersProps) {
7070
<section
7171
id='open-roles'
7272
aria-labelledby='open-roles-heading'
73-
className='mx-auto flex w-full max-w-[1460px] flex-col gap-10 px-20 pt-6 pb-24 max-sm:px-5 max-sm:pb-16 max-lg:px-8'
73+
className='mx-auto flex w-full max-w-[1728px] flex-col gap-10 px-10 pt-6 pb-24 max-sm:pb-16 max-md:px-7 max-lg:px-8 max-xl:px-9'
7474
>
7575
<h2
7676
id='open-roles-heading'

apps/sim/app/(landing)/comparisons/[provider]/page.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {
1717
SIM_LATEST_VERIFIED,
1818
} from '@/app/(landing)/comparisons/utils'
1919
import { BackLink } from '@/app/(landing)/components'
20-
import { Cta } from '@/app/(landing)/components/cta/cta'
2120
import { JsonLd } from '@/app/(landing)/components/json-ld'
2221
import { LandingFAQ } from '@/app/(landing)/components/landing-faq'
2322

@@ -317,10 +316,6 @@ export default async function ComparisonProviderPage({
317316

318317
<div className='-mt-px h-px w-full bg-[var(--border)]' />
319318
</main>
320-
321-
<div className='py-16'>
322-
<Cta />
323-
</div>
324319
</>
325320
)
326321
}

0 commit comments

Comments
 (0)