diff --git a/.claude/rules/emcn-components.md b/.claude/rules/emcn-components.md index 50e7684363f..cab88b47f67 100644 --- a/.claude/rules/emcn-components.md +++ b/.claude/rules/emcn-components.md @@ -20,7 +20,7 @@ The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items ## Component catalogue -- **`Chip` / `ChipLink`** — the pill button (` + ), + PasswordInput: ({ + error, + ...props + }: InputHTMLAttributes & { error?: boolean }) => , + 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( + + ) + ) + const fields = { name: 'Test Builder', email: 'builder@example.com', password: 'SafePass1!' } + for (const [name, value] of Object.entries(fields)) { + const input = host.querySelector(`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('') + }) +}) diff --git a/apps/sim/app/(auth)/signup/signup-form.tsx b/apps/sim/app/(auth)/signup/signup-form.tsx index 7488520915a..fcdbc0f13b8 100644 --- a/apps/sim/app/(auth)/signup/signup-form.tsx +++ b/apps/sim/app/(auth)/signup/signup-form.tsx @@ -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) diff --git a/apps/sim/app/(auth)/verify/use-verification.ts b/apps/sim/app/(auth)/verify/use-verification.ts index c2fbe5d4dbb..5927438998b 100644 --- a/apps/sim/app/(auth)/verify/use-verification.ts +++ b/apps/sim/app/(auth)/verify/use-verification.ts @@ -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' @@ -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('') @@ -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, diff --git a/apps/sim/app/(interfaces)/components/interfaces-shell/interfaces-shell.tsx b/apps/sim/app/(interfaces)/components/interfaces-shell/interfaces-shell.tsx index c3ea3a614c3..e7488618390 100644 --- a/apps/sim/app/(interfaces)/components/interfaces-shell/interfaces-shell.tsx +++ b/apps/sim/app/(interfaces)/components/interfaces-shell/interfaces-shell.tsx @@ -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, diff --git a/apps/sim/app/(interfaces)/resume/[workflowId]/[executionId]/resume-execution-unavailable.tsx b/apps/sim/app/(interfaces)/resume/[workflowId]/[executionId]/resume-execution-unavailable.tsx index 52c9d065127..890d122292f 100644 --- a/apps/sim/app/(interfaces)/resume/[workflowId]/[executionId]/resume-execution-unavailable.tsx +++ b/apps/sim/app/(interfaces)/resume/[workflowId]/[executionId]/resume-execution-unavailable.tsx @@ -1,4 +1,4 @@ -import { ChipLink } from '@sim/emcn' +import { chipContentLabelClass, chipVariants, cn } from '@sim/emcn' export function ResumeExecutionUnavailable() { return ( @@ -8,9 +8,9 @@ export function ResumeExecutionUnavailable() {

This execution could not be located or has already completed.

- - Return Home - + + Return Home + ) diff --git a/apps/sim/app/(landing)/CLAUDE.md b/apps/sim/app/(landing)/CLAUDE.md index 16c31795602..b724d10693e 100644 --- a/apps/sim/app/(landing)/CLAUDE.md +++ b/apps/sim/app/(landing)/CLAUDE.md @@ -6,22 +6,22 @@ This route group owns `/` and the entire public marketing surface - the home pag ## What this is -- `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 `
` content. +- `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 `
` content. The painted light/dark CTA and footer are owned by `LandingShell`; never add page-specific closing CTA bands or footer instances. - 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. -## Styling - draw from the platform's light mode +## Styling - draw from the platform's tokens -The landing page looks like the product. Its visual language is the workspace UI in light mode, not a separate marketing theme. +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. -- **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. -- **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. +- **Light by default, dark on request.** The landing family follows the theme class on `` (next-themes, storage key `sim-landing-theme` - its own store, separate from the workspace's account-synced `sim-theme`, which holds `system` for every signed-in user): a visitor who has not chosen otherwise here 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. +- **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` (its legacy alias `--border-1` is not for new work); brand `--brand-agent` / `--brand-secondary` / `--brand-accent`. Do **not** use the legacy `--landing-*` tokens - they belong to the old dark landing. - **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`. - **Typography is the platform's.** Season is the global body font (`font-season` is applied on `` 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. - **Never touch global styles.** No additions to `app/_styles/globals.css`. All styling is local Tailwind classes; `cn()` from `@/lib/core/utils/cn` for conditionals; no inline `style` attributes. - **Responsive - desktop is the source of truth, scaled down via `max-*` overrides.** The page is fully responsive (iPad + phone). The desktop layout stays the unprefixed baseline; smaller screens are handled by *layering* `max-*` overrides on top, so desktop renders byte-identically. Tiers: - `max-xl:` (≤1279) - the hero's two-panel split (absolute visual + logos) collapses to a stacked, in-flow column. The split needs ≥1280 to avoid the headline colliding with the visual panel; iPad-landscape (1024) therefore gets the stacked hero with the desktop nav. - - `max-lg:` (≤1023) - the desktop nav clusters hide (`hidden lg:flex`) and `MobileNav` (hamburger sheet) takes over; multi-column grids step down (mothership 4→2, footer 7→3); shared gutter `px-20 → max-lg:px-8`; section gaps tighten. - - `max-md:` (≤767) - Features beats drop the floating callout (`max-md:hidden`) and show the un-masked backdrop preview full-width. + - `max-lg:` (≤1023) - the desktop nav clusters hide (`hidden lg:flex`) and `MobileNav` (hamburger sheet) takes over; multi-column grids step down (footer 7→3); the shared gutter narrows; section gaps tighten. + - `max-md:` (≤767) - two-up feature and card rows stack to one column. - `max-sm:` (≤639) - single-column grids, smallest type scale, `px-5` gutter, hero CTA row stacks. When adding a new section, give it the same `px-20 max-lg:px-8 max-sm:px-5` gutter so the navbar wordmark stays aligned with section content at every width. Verify desktop is unchanged and there is zero horizontal overflow at 1280 / 1024 / 768 / 390 before shipping. @@ -79,8 +79,8 @@ Follow `.claude/rules/constitution.md` exactly: Sim is "the open-source AI works └── components/ ├── index.ts # top barrel ├── navbar/{navbar.tsx, index.ts, components//…} #