AuthSignInSplit

Split-screen sign-in page. Marketing panel on the left (≥lg), credential form on the right. Routes every credential / OAuth call through a consumer-supplied AuthAdapter.

Installation

pnpm
pnpm dlx @hex-core/cli add auth-sign-in-split

Usage

tsx
import { AuthSignInSplit } from "@/components/ui/auth-sign-in-split"

Mock adapter (showcase / tests)

Demo with the in-memory mock adapter — every method resolves ok:true after 400ms.

tsx
import { AuthSignInSplit, mockAuthAdapter } from "@hex-core/components";

<AuthSignInSplit
  adapter={mockAuthAdapter}
  brand={<strong>Acme</strong>}
  marketing="The fastest way to ship spec-driven UI."
  socialProviders={[
    { provider: "github", label: "GitHub" },
    { provider: "google", label: "Google" },
  ]}
/>

Real provider (better-auth)

Wire better-auth's email/password and social methods through the AuthAdapter contract.

tsx
import { AuthSignInSplit, type AuthAdapter } from "@hex-core/components";
import { authClient } from "@/lib/auth-client";

const adapter: AuthAdapter = {
  async signInWithPassword({ email, password, remember }) {
    const res = await authClient.signIn.email({ email, password, rememberMe: remember });
    if (res.error) return { ok: false, error: { code: res.error.code, message: res.error.message } };
    return { ok: true, redirect: "/app" };
  },
  async signInWithSocial({ provider }) {
    await authClient.signIn.social({ provider });
    return { ok: true };
  },
};

<AuthSignInSplit adapter={adapter} />

API Reference

PropTypeDefaultDescription
adapterrequired
objectAuthAdapter implementation. The block calls adapter.signInWithPassword and (if socialProviders are passed) adapter.signInWithSocial. Hex Core never touches credentials directly.
socialProviders
objectReadonlyArray<{ provider: AuthSocialProvider; label: string; icon?: ReactNode }>. List of social-login buttons rendered above the email field. The `provider` value is forwarded verbatim to adapter.signInWithSocial({ provider }). Pass an empty array or omit the prop to hide the social section and the 'or' divider.
brand
ReactNodeBrand block (logo + product name) shown at the top of the marketing panel.
marketing
ReactNodeMarketing copy / quote / illustration shown below the brand block.
signUpHref
string/sign-upHref for the 'Sign up' link rendered below the form.
forgotPasswordHref
string/forgot-passwordHref for the 'Forgot?' link inline with the password label.
className
stringAdditional classes applied to the root grid wrapper.
onSuccess
functionCalled after a successful sign-in (any flow) with the adapter's redirect target: (redirect: string | undefined) => void.

AI Guidance

When to use

Use as the default sign-in page when you have meaningful marketing copy or a brand panel to show alongside the form. The split layout earns its keep on desktop; on mobile it collapses cleanly to the form-only view.

When not to use

Don't use for in-app re-auth modals (use a Dialog with form fields instead). Don't use when the marketing panel would be empty — drop to a centered single-column block.

Common mistakes

  • Forgetting to pass the AuthAdapter — the block has no built-in fallback and will surface 'unimplemented' errors when the user submits.
  • Hard-coding signUpHref / forgotPasswordHref to absolute URLs — they're plain anchor hrefs, prefer in-app relative paths so the consumer's router handles navigation.
  • Skipping onSuccess and relying on adapter.redirect alone — the redirect is informational; the consumer's onSuccess handler is what actually navigates.
  • Wiring auth state inside the adapter methods instead of returning { ok, error } — the block reads the result and shows the error Alert; throwing inside the adapter loses the structured error payload.

Accessibility

Form inputs have explicit Label htmlFor pairing, autoComplete='email' / 'current-password', and required attributes. Submit button uses the canonical loading prop (sets aria-busy + disabled). Errors render in an Alert with role='alert' so they're announced. Marketing aside is aria-hidden so screen-reader users don't traverse decorative copy before reaching the form.

Token budget: 1800