Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
979e49f
feat(landing): deliver phase one marketing redesign
andresdjasso Sep 5, 2026
7237f81
fix(landing): showcase supported CLI workflows
andresdjasso Sep 6, 2026
21306ef
fix(landing): smooth featured customer transitions
andresdjasso Sep 7, 2026
7cbaa8c
feat(landing): refresh product pages with native UI previews
andresdjasso Sep 8, 2026
181ec0b
fix(landing): allow page scrolling over product previews
andresdjasso Sep 8, 2026
1983926
Merge remote-tracking branch 'origin/staging' into improvement/market…
andresdjasso Sep 8, 2026
bc1776b
fix(landing): harden preview keyboard interactions
andresdjasso Sep 8, 2026
ac51f9c
fix(landing): restore bundle, theme, and analytics guarantees in the …
waleedlatif1 Sep 8, 2026
e60189f
refactor(landing): simplify the redesign after review
waleedlatif1 Sep 8, 2026
ae92a05
chore(landing): apply the cleanup pass
waleedlatif1 Sep 8, 2026
20acfb9
fix(landing): address reviewer feedback and audit findings
waleedlatif1 Sep 8, 2026
3a4dba5
fix(landing): address QA findings and isolate demo dependencies
waleedlatif1 Sep 8, 2026
a372f33
fix(landing): enable drag and trackpad scrolling in customer carousel
j15z Sep 8, 2026
143ea40
fix(landing): fit account links in narrow mobile menus
j15z Sep 8, 2026
cd552e7
Merge remote-tracking branch 'origin/staging' into codex/landing-qa-f…
waleedlatif1 Sep 8, 2026
5099a1b
fix(landing): finish accessibility and routing QA
waleedlatif1 Sep 8, 2026
5fa3892
fix(test): align enrollment shell mock with direct import
waleedlatif1 Sep 8, 2026
744a01b
refactor(ui): use the narrow logo shell barrel
waleedlatif1 Sep 8, 2026
360b12a
Merge remote-tracking branch 'origin/staging' into codex/landing-qa-f…
waleedlatif1 Sep 8, 2026
38f4664
Merge remote-tracking branch 'origin/staging' into codex/landing-qa-f…
waleedlatif1 Sep 8, 2026
a54822e
fix(landing): refine graphics, navigation, and accessibility
waleedlatif1 Sep 8, 2026
9a2131f
fix(landing): align preview controls and simplify CLI copy
waleedlatif1 Sep 8, 2026
4228d63
chore(review): remove ineffective file-limit override
waleedlatif1 Sep 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .claude/rules/emcn-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items

## Component catalogue

- **`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`.
- **`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`.
- **`ChipInput`** — single-line text field. `icon`, `endAdornment`, `error`, `inputClassName` (inner `<input>`); `className` styles the chrome wrapper.
- **`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.
- **`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`).
Expand Down
24 changes: 24 additions & 0 deletions apps/sim/app/(auth)/components/auth-shell.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/** @vitest-environment node */
import type { ReactNode } from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { expect, it, vi } from 'vitest'
import { AuthShell } from '@/app/(auth)/components/auth-shell'

vi.mock('next/link', () => ({
default: ({ children }: { children: ReactNode }) => (
<a href='/' data-client-navigation>
{children}
</a>
),
}))
vi.mock('@/app/_shell/desktop-title-bar', () => ({ DesktopTitleBarLane: () => null }))
vi.mock('@/app/(landing)/components/navbar/components', () => ({
LogoMark: ({ children }: { children: ReactNode }) => <>{children}</>,
SimWordmark: () => 'Sim',
}))

it('returns home through a document link so route-specific theme defaults reinitialize', () => {
const html = renderToStaticMarkup(<AuthShell>Sign in</AuthShell>)
expect(html).toContain('href="/" aria-label="Sim home"')
expect(html).not.toContain('data-client-navigation')
})
7 changes: 4 additions & 3 deletions apps/sim/app/(auth)/components/auth-shell.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { ReactNode } from 'react'
import Link from 'next/link'
import { DesktopTitleBarLane } from '@/app/_shell/desktop-title-bar'
import { LogoMark, SimWordmark } from '@/app/(landing)/components/navbar/components'

Expand All @@ -19,6 +18,8 @@ interface AuthShellProps {
* the canvas/`--text-primary` surface, and renders a logo-only header that reuses
* the landing {@link LogoMark} + {@link SimWordmark} at the same nav gutters. The
* single content column is centered and capped for a calm single-form layout.
* The home link starts a document navigation so the marketing theme default is
* initialized independently of auth's forced-light context.
*
* The shell also owns the macOS traffic-light lane, unconditionally — every surface that
* wears it (the `(auth)` routes, the CLI auth handoff, the invite pages) sits outside
Expand All @@ -34,11 +35,11 @@ export function AuthShell({ children, footer }: AuthShellProps) {
<DesktopTitleBarLane />
<header>
<nav className='mx-auto flex w-full max-w-[1446px] items-center px-12 py-4 max-sm:px-5 max-lg:px-8'>
<Link href='/' aria-label='Sim home' className='flex h-[30px] items-center'>
<a href='/' aria-label='Sim home' className='flex h-[30px] items-center'>
<LogoMark>
<SimWordmark />
</LogoMark>
</Link>
</a>
</nav>
</header>
<div className='flex flex-1 items-center justify-center px-4 pb-16'>
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/app/(auth)/login/login-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,8 @@ export default function LoginPage({
// Clear reset success message on successful login
setResetSuccessMessage(null)

// Explicit redirect fallback if better-auth doesn't redirect
router.push(safeCallbackUrl)
/** Fallback when better-auth does not redirect: a document navigation, like signup's, so the workspace shell initializes its own theme store. */
window.location.href = safeCallbackUrl
} catch (err: any) {
if (err.message?.includes('not verified') || err.code?.includes('EMAIL_NOT_VERIFIED')) {
redirectToVerify(email)
Expand Down
132 changes: 132 additions & 0 deletions apps/sim/app/(auth)/signup/signup-form.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/** @vitest-environment jsdom */
import { act, type InputHTMLAttributes, type ReactNode } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import SignupForm from '@/app/(auth)/signup/signup-form'

const { push, signUp, refetchSession } = vi.hoisted(() => ({
push: vi.fn(),
signUp: vi.fn(),
refetchSession: vi.fn(),
}))

vi.mock('next/navigation', () => ({
useRouter: () => ({ push }),
useSearchParams: () => new URLSearchParams(),
}))
vi.mock('@marsidev/react-turnstile', () => ({ Turnstile: () => null }))
vi.mock('posthog-js/react', () => ({ usePostHog: () => null }))
vi.mock('@/lib/analytics/google', () => ({ trackGoogleEvent: vi.fn() }))
vi.mock('@/lib/auth/auth-client', () => ({
client: { signUp: { email: signUp } },
useSession: () => ({ refetch: refetchSession }),
}))
vi.mock('@/lib/consent/tracking-consent', () => ({
useTrackingConsent: () => ({ measurement: false }),
}))
vi.mock('@/lib/core/config/env', () => ({ getEnv: () => undefined, isFalsy: () => false }))
vi.mock('@/lib/core/config/env-flags', () => ({ isSsoEnabled: false }))
vi.mock('@/lib/core/security/input-validation', () => ({ validateCallbackUrl: () => false }))
vi.mock('@/lib/messaging/email/validation', () => ({
quickValidateEmail: () => ({ isValid: true }),
}))
vi.mock('@/lib/posthog/client', () => ({ captureClientEvent: vi.fn(), captureEvent: vi.fn() }))
vi.mock('@/app/(auth)/components', () => ({
AuthDivider: () => null,
AuthField: ({ children }: { children: ReactNode }) => <>{children}</>,
AuthFormMessage: () => null,
AuthHeader: () => null,
AuthInput: ({ error, ...props }: InputHTMLAttributes<HTMLInputElement> & { error?: boolean }) => (
<input {...props} />
),
AuthLegalFooter: () => null,
AuthNavPrompt: () => null,
AuthSubmitButton: ({ children }: { children: ReactNode }) => (
<button type='submit'>{children}</button>
),
PasswordInput: ({
error,
...props
}: InputHTMLAttributes<HTMLInputElement> & { error?: boolean }) => <input {...props} />,
SocialLoginButtons: () => null,
SSOLoginButton: () => null,
}))

let root: Root
let host: HTMLDivElement
let destination: { href: string }

beforeEach(() => {
vi.clearAllMocks()
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
destination = { href: '' }
const browser = window
vi.stubGlobal(
'window',
new Proxy(browser, {
get(target, key) {
return key === 'location' ? destination : Reflect.get(target, key, target)
},
})
)
signUp.mockResolvedValue({ data: { user: { id: 'new-user' } } })
refetchSession.mockResolvedValue(undefined)
host = document.createElement('div')
document.body.append(host)
root = createRoot(host)
})

afterEach(() => {
act(() => root.unmount())
host.remove()
vi.unstubAllGlobals()
})

async function submit(emailVerificationEnabled: boolean) {
act(() =>
root.render(
<SignupForm
githubAvailable={false}
googleAvailable={false}
microsoftAvailable={false}
emailSignupEnabled
emailVerificationEnabled={emailVerificationEnabled}
/>
)
)
const fields = { name: 'Test Builder', email: 'builder@example.com', password: 'SafePass1!' }
for (const [name, value] of Object.entries(fields)) {
const input = host.querySelector<HTMLInputElement>(`input[name="${name}"]`)
if (!input) throw new Error(`Missing ${name} input`)
input.value = value
}
const form = host.querySelector('form')
if (!form) throw new Error('Missing signup form')
await act(async () =>
form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
)
}

describe('signup shell navigation', () => {
it('starts a document navigation after a successful signup without verification', async () => {
await submit(false)
expect(signUp).toHaveBeenCalledOnce()
expect(refetchSession).toHaveBeenCalledOnce()
expect(destination.href).toBe('/home')
expect(push).not.toHaveBeenCalled()
})

it('keeps verification within the auth shell and stores the email for the next step', async () => {
await submit(true)
expect(push).toHaveBeenCalledWith('/verify?fromSignup=true')
expect(sessionStorage.getItem('verificationEmail')).toBe('builder@example.com')
expect(destination.href).toBe('')
})

it('does not navigate when signup fails', async () => {
signUp.mockResolvedValue({ error: { message: 'Signup failed' } })
await submit(false)
expect(push).not.toHaveBeenCalled()
expect(destination.href).toBe('')
})
})
8 changes: 3 additions & 5 deletions apps/sim/app/(auth)/signup/signup-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -372,12 +372,10 @@ function SignupFormContent({

if (destination.kind === 'verify') {
router.push(VERIFY_FROM_SIGNUP_ROUTE)
} else if (destination.kind === 'redirect') {
// Full navigation, matching the verify hop: the destination (invite, CLI
// handoff) is server-rendered and must see the fresh session cookie.
window.location.href = destination.url
} else {
router.push(DEFAULT_POST_AUTH_ROUTE)
/** Match login/verification: refresh session-bound shells and their theme default. */
window.location.href =
destination.kind === 'redirect' ? destination.url : DEFAULT_POST_AUTH_ROUTE
}
} catch (error) {
logger.error('Signup error:', error)
Expand Down
12 changes: 4 additions & 8 deletions apps/sim/app/(auth)/verify/use-verification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { useEffect, useState } from 'react'
import { createLogger } from '@sim/logger'
import { normalizeEmail } from '@sim/utils/string'
import { useRouter, useSearchParams } from 'next/navigation'
import { useSearchParams } from 'next/navigation'
import { client, useSession } from '@/lib/auth/auth-client'
import { validateCallbackUrl } from '@/lib/core/security/input-validation'
import { DEFAULT_POST_AUTH_ROUTE, POST_AUTH_REDIRECT_STORAGE_KEY } from '@/app/(auth)/auth-redirect'
Expand Down Expand Up @@ -74,7 +74,6 @@ export function useVerification({
isProduction,
isEmailVerificationEnabled,
}: UseVerificationParams): UseVerificationReturn {
const router = useRouter()
const searchParams = useSearchParams()
const { refetch: refetchSession } = useSession()
const [otp, setOtp] = useState('')
Expand Down Expand Up @@ -215,15 +214,12 @@ export function useVerification({
logger.warn('Failed to refetch session during verification skip:', error)
}

if (destination) {
window.location.href = destination
} else {
router.push(DEFAULT_POST_AUTH_ROUTE)
}
/** A document navigation, like signup's, so the workspace shell initializes its own theme store. */
window.location.href = destination ?? DEFAULT_POST_AUTH_ROUTE
}

handleRedirect()
}, [isEmailVerificationEnabled, router, searchParams])
}, [isEmailVerificationEnabled, searchParams])

return {
otp,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ReactNode } from 'react'
import { SupportFooter } from '@/app/(auth)/components'
import { LogoShell } from '@/app/(landing)/components'
import { LogoShell } from '@/app/(landing)/components/logo-shell'

/**
* Chrome for the `(interfaces)` route group (chat + resume) — the lightweight,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChipLink } from '@sim/emcn'
import { chipContentLabelClass, chipVariants, cn } from '@sim/emcn'

export function ResumeExecutionUnavailable() {
return (
Expand All @@ -8,9 +8,9 @@ export function ResumeExecutionUnavailable() {
<p className='mb-6 text-[var(--text-secondary)] text-sm'>
This execution could not be located or has already completed.
</p>
<ChipLink variant='border' href='/'>
Return Home
</ChipLink>
<a href='/' className={chipVariants({ variant: 'border' })}>
<span className={cn(chipContentLabelClass, 'flex-1')}>Return Home</span>
</a>
</div>
</div>
)
Expand Down
Loading
Loading