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 dlx @hex-core/cli add auth-sign-in-splitUsage
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.
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.
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
| Prop | Type | Default | Description |
|---|---|---|---|
adapterrequired | object | — | AuthAdapter implementation. The block calls adapter.signInWithPassword and (if socialProviders are passed) adapter.signInWithSocial. Hex Core never touches credentials directly. |
socialProviders | object | — | ReadonlyArray<{ 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 | ReactNode | — | Brand block (logo + product name) shown at the top of the marketing panel. |
marketing | ReactNode | — | Marketing copy / quote / illustration shown below the brand block. |
signUpHref | string | /sign-up | Href for the 'Sign up' link rendered below the form. |
forgotPasswordHref | string | /forgot-password | Href for the 'Forgot?' link inline with the password label. |
className | string | — | Additional classes applied to the root grid wrapper. |
onSuccess | function | — | Called 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
Verified against @hex-core/components@1.9.0