Skip to content

Commit b3eb2ca

Browse files
andresdjassowaleedlatif1claudej15z
authored
feat(landing): redesign marketing and product pages (#7543)
* feat(landing): deliver phase one marketing redesign * fix(landing): showcase supported CLI workflows * fix(landing): smooth featured customer transitions * feat(landing): refresh product pages with native UI previews * fix(landing): allow page scrolling over product previews * fix(landing): harden preview keyboard interactions * fix(landing): restore bundle, theme, and analytics guarantees in the redesign - Lazy-mount the hero product loop (ssr: false) so the workspace chat graph and the full block registry no longer ship in every marketing route's initial bundle (1.15 MB gz per route, down from 3.44 MB); load the React Flow stage on demand - Give the marketing surface its own theme store (`sim-landing-theme`): the workspace settings sync writes `system` into `sim-theme` for every signed-in user, which rendered sim.ai in the OS theme and overwrote the footer toggle; make the remaining home links from forced-light shells document navigations - Track the shell's closing CTA (`footer_cta`) so integration pages keep a tracked closing conversion; harden external CTA links in one place - Restore `priority` on the hero LCP image; stream the customer film only while on screen in a visible tab and stop fetching its poster twice - Delete ~5,800 lines of unreachable components, dead props, and orphaned assets the redesign left behind; share the seeded chat strings; reattach two TSDocs Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nwwczp7n4NrACeonLvEgw5 * refactor(landing): simplify the redesign after review - Stop preview loops while their section is off screen or the tab is hidden - Drop dead tile/tone branches from the six feature graphics, the unread product-layout discriminant, `logoOnly`, placement-frame props, and unused layout/type exports; share the two-block workflow preview and the elapsed-reveal hook; use the shared `noop` - Make the invite pages' home buttons document navigations so the marketing theme store initializes; drop a no-op grayscale filter on the hero image Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nwwczp7n4NrACeonLvEgw5 * chore(landing): apply the cleanup pass - Use the canonical `--border` token in the new marketing components, `cn()` for the banner class list, and the chip `shape` prop instead of a radius override - Replace an undefined `--brand-primary` on the qualified-lead dot with the brand accent, and an undefined `--shadow-sm` with the utility - Drop an unobserved `useCallback` and a dead handle-offset constant Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nwwczp7n4NrACeonLvEgw5 * fix(landing): address reviewer feedback and audit findings Navbar: pin the root's overscroll while the scroll port is locked so an open menu cannot rubber-band the page; drop the press scale on menu triggers that do not navigate; lay integration tiles out as rows so the preview fade no longer covers their names; hide the auth pill in the phone header. Hero and stages: draw the intro frame on the region's border instead of one pixel inside it; remove the unshipped Build control from the hero composer; give the workflows editor loop its block type labels; use the Human block's icon and fields on the enterprise stage; size React Flow nodes at mount. Pages: round the changelog chips; give compact pill CTAs the same inset as the larger sizes; soften the footer wordmark's thresholded edge. Review follow-ups: keep the landing theme key through logout, make the login fallback a document navigation, remove a shadow the design never had, and restore the chip label chrome on two document home links. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(landing): address QA findings and isolate demo dependencies * fix(landing): enable drag and trackpad scrolling in customer carousel Scrub the existing logo loop with pointer drags, horizontal trackpad gestures, and arrow keys while preserving autoplay position and native trackpad momentum. Keep reduced-motion support and normal vertical scrolling and pinch-to-zoom behavior. * fix(landing): fit account links in narrow mobile menus Size the account pill segments around their labels and reduce minimum horizontal padding so Start building stays inside its segment on narrow screens. * fix(landing): finish accessibility and routing QA * fix(test): align enrollment shell mock with direct import * refactor(ui): use the narrow logo shell barrel * fix(landing): refine graphics, navigation, and accessibility * fix(landing): align preview controls and simplify CLI copy * chore(review): remove ineffective file-limit override --------- Co-authored-by: andresdjasso <andresdjasso@users.noreply.github.com> Co-authored-by: Waleed Latif <walif6@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Justin Blumencranz <96924014+j15z@users.noreply.github.com>
1 parent 9b1d6ed commit b3eb2ca

505 files changed

Lines changed: 22112 additions & 9893 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.

.claude/rules/emcn-components.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items
2020

2121
## Component catalogue
2222

23-
- **`Chip` / `ChipLink`** — the pill button (`<button>` / Next `<Link>`). Variants: `primary`, `destructive`, `border-shadow`, `border`; the bare chip is implicit (omit `variant`). `filled` is deliberately NOT a `Chip` variant — it is reserved for chip fields/triggers. For a selected/toggle chip use the `active` prop, never a variant. `leftIcon`/`rightIcon`, `active`, `fullWidth`. Chips carry **no outer margin** — space between them is the parent's `gap`. The old `mx-0.5` default and its `flush` opt-out are gone; do not reintroduce either, and never add a margin to a chip through `className`.
23+
- **`Chip` / `ChipLink`** — the pill button (`<button>` / Next `<Link>`). Variants: `primary`, `destructive`, `border-shadow`, `border`, `outline` (a true `--border` border, no shadow or hover fill); the bare chip is implicit (omit `variant`). `filled` is deliberately NOT a `Chip` variant — it is reserved for chip fields/triggers. For a selected/toggle chip use the `active` prop, never a variant. `leftIcon`/`rightIcon`, `active`, `fullWidth`. Chips carry **no outer margin** — space between them is the parent's `gap`. The old `mx-0.5` default and its `flush` opt-out are gone; do not reintroduce either, and never add a margin to a chip through `className`.
2424
- **`ChipInput`** — single-line text field. `icon`, `endAdornment`, `error`, `inputClassName` (inner `<input>`); `className` styles the chrome wrapper.
2525
- **`ChipCopyInput`** — the canonical view-only field: a read-only `ChipInput` at full opacity with a trailing copy-to-clipboard button. View-only is a display mode, not a disabled state — reach for it (or `ChipModalField type='copy'`) over a `disabled` (greyed) input for values the user cannot edit.
2626
- **`ChipTextarea`** — multi-line sibling. `error`, `resizable` (off by default), `viewOnly` (read-only at full opacity with the default cursor — the multi-line counterpart of `ChipCopyInput`).
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'>

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,8 +262,8 @@ export default function LoginPage({
262262
// Clear reset success message on successful login
263263
setResetSuccessMessage(null)
264264

265-
// Explicit redirect fallback if better-auth doesn't redirect
266-
router.push(safeCallbackUrl)
265+
/** Fallback when better-auth does not redirect: a document navigation, like signup's, so the workspace shell initializes its own theme store. */
266+
window.location.href = safeCallbackUrl
267267
} catch (err: any) {
268268
if (err.message?.includes('not verified') || err.code?.includes('EMAIL_NOT_VERIFIED')) {
269269
redirectToVerify(email)
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('/home')
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/(auth)/verify/use-verification.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { useEffect, useState } from 'react'
44
import { createLogger } from '@sim/logger'
55
import { normalizeEmail } from '@sim/utils/string'
6-
import { useRouter, useSearchParams } from 'next/navigation'
6+
import { useSearchParams } from 'next/navigation'
77
import { client, useSession } from '@/lib/auth/auth-client'
88
import { validateCallbackUrl } from '@/lib/core/security/input-validation'
99
import { DEFAULT_POST_AUTH_ROUTE, POST_AUTH_REDIRECT_STORAGE_KEY } from '@/app/(auth)/auth-redirect'
@@ -74,7 +74,6 @@ export function useVerification({
7474
isProduction,
7575
isEmailVerificationEnabled,
7676
}: UseVerificationParams): UseVerificationReturn {
77-
const router = useRouter()
7877
const searchParams = useSearchParams()
7978
const { refetch: refetchSession } = useSession()
8079
const [otp, setOtp] = useState('')
@@ -215,15 +214,12 @@ export function useVerification({
215214
logger.warn('Failed to refetch session during verification skip:', error)
216215
}
217216

218-
if (destination) {
219-
window.location.href = destination
220-
} else {
221-
router.push(DEFAULT_POST_AUTH_ROUTE)
222-
}
217+
/** A document navigation, like signup's, so the workspace shell initializes its own theme store. */
218+
window.location.href = destination ?? DEFAULT_POST_AUTH_ROUTE
223219
}
224220

225221
handleRedirect()
226-
}, [isEmailVerificationEnabled, router, searchParams])
222+
}, [isEmailVerificationEnabled, searchParams])
227223

228224
return {
229225
otp,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ReactNode } from 'react'
22
import { SupportFooter } from '@/app/(auth)/components'
3-
import { LogoShell } from '@/app/(landing)/components'
3+
import { LogoShell } from '@/app/(landing)/components/logo-shell'
44

55
/**
66
* Chrome for the `(interfaces)` route group (chat + resume) — the lightweight,

apps/sim/app/(interfaces)/resume/[workflowId]/[executionId]/resume-execution-unavailable.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ChipLink } from '@sim/emcn'
1+
import { chipContentLabelClass, chipVariants, cn } from '@sim/emcn'
22

33
export function ResumeExecutionUnavailable() {
44
return (
@@ -8,9 +8,9 @@ export function ResumeExecutionUnavailable() {
88
<p className='mb-6 text-[var(--text-secondary)] text-sm'>
99
This execution could not be located or has already completed.
1010
</p>
11-
<ChipLink variant='border' href='/'>
12-
Return Home
13-
</ChipLink>
11+
<a href='/' className={chipVariants({ variant: 'border' })}>
12+
<span className={cn(chipContentLabelClass, 'flex-1')}>Return Home</span>
13+
</a>
1414
</div>
1515
</div>
1616
)

0 commit comments

Comments
 (0)