Login02
A Auth block/component from shadcn/ui — clean-room reimplemented for Domphy (see methodology). Call Login02() with no arguments for a working demo, or edit the code below live.
Props
| Prop | Type | Description |
|---|---|---|
brandName | string | — |
heading | string | — |
description | string | — |
emailLabel | string | — |
emailPlaceholder | string | — |
passwordLabel | string | — |
forgotPasswordHref | string | — |
primaryButtonLabel | string | — |
dividerText | string | — |
githubButtonLabel | string | — |
onGithubClick | () => void | — |
signUpPrompt | string | — |
signUpLabel | string | — |
signUpHref | string | — |
coverImageSrc | string | — |
coverImageAlt | string | — |
dimCoverInDarkMode | boolean | — |
onSubmit | (values: { email: string; password: string }) => void | — |
Implementation notes
Full page-level 2-col grid (brand row + centered form column | full-bleed cover photo), image column hidden and grid collapses to 1 col at max-width 47.9375em via @media. Dark-mode image dimming via prefers-color-scheme media query (brightness+saturate filter), discrete not animated. GitHub glyph is a simplified hand-authored octocat-style silhouette, original geometry. Doctor diagnose(): 0 findings. Direct-source-diff fix (2026-07-05): Cover-image column collapsed at the wrong breakpoint (md/768px) — upstream login-02 collapses at lg/1024px. Added a dedicated WIDE_SPLIT_MEDIA_QUERY and switched this recipe to it.
Status: ported · Reference: shadcn/ui original
// shadcn/ui "login-02" block — clean-room reimplementation.
//
// A split-screen sign-in page: the form fills the left half of the
// viewport, a full-bleed cover photo fills the right half. The image
// column is hidden below a mid-size breakpoint, leaving only the form.
// See ./login01-05-shared.ts for the reusable field/button/divider pieces.
//
// Implemented purely from the block's public functional/visual spec — no
// upstream shadcn/ui source was viewed or copied.
import type { DomphyElement } from "@domphy/core";
import { themeSpacing } from "@domphy/theme";
import { heading, paragraph } from "@domphy/ui";
import { fixed } from "../../shared/typography.js";
import {
brandBadge,
coverImage,
coverPanel,
dividerRow,
emailField,
oauthButton,
passwordField,
signUpLine,
submitButton,
WIDE_SPLIT_MEDIA_QUERY,
} from "./login01-05-shared.js";
/** Props for {@link Login02}. */
export interface Login02Props {
brandName?: string;
heading?: string;
description?: string;
emailLabel?: string;
emailPlaceholder?: string;
passwordLabel?: string;
forgotPasswordHref?: string;
primaryButtonLabel?: string;
dividerText?: string;
githubButtonLabel?: string;
onGithubClick?: () => void;
signUpPrompt?: string;
signUpLabel?: string;
signUpHref?: string;
coverImageSrc?: string;
coverImageAlt?: string;
dimCoverInDarkMode?: boolean;
onSubmit?: (values: { email: string; password: string }) => void;
}
/**
* shadcn/ui "login-02" — split-screen sign-in with a full-bleed cover photo.
* Call with no arguments for a fully working demo.
*/
function Login02(props: Login02Props = {}): DomphyElement<"div"> {
const {
brandName = "Acme Inc.",
heading: headingText = "Login to your account",
description = "Enter your email below to login to your account",
emailLabel = "Email",
emailPlaceholder = "m@example.com",
passwordLabel = "Password",
forgotPasswordHref = "#",
primaryButtonLabel = "Login",
dividerText = "Or continue with",
githubButtonLabel = "Login with GitHub",
onGithubClick,
signUpPrompt = "Don't have an account?",
signUpLabel = "Sign up",
signUpHref = "#",
coverImageSrc,
coverImageAlt = "",
dimCoverInDarkMode = true,
onSubmit,
} = props;
// Upstream wraps the badge + wordmark in a clickable `<a href="#"
// className="flex items-center gap-2 font-medium">` — a link, medium
// weight (not a bold non-interactive div). `color`/`textDecoration` reset
// the browser's default anchor styling so the wordmark reads as plain
// foreground text, matching the source.
const brandRow: DomphyElement<"a"> = {
a: [brandBadge(), brandName],
href: "#",
style: {
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: themeSpacing(2),
fontWeight: fixed("500"),
color: "inherit",
textDecoration: fixed("none"),
"@media (min-width: 48em)": { justifyContent: "flex-start" },
},
};
const formBlock: DomphyElement<"form"> = {
form: [
// Upstream groups the h1 + p in a
// `<div className="flex flex-col items-center gap-1 text-center">`
// so both are centered with a 4px (gap-1) gap.
{
div: [
{ h1: headingText, $: [heading()] },
{ p: description, $: [paragraph({ color: "neutral" })] },
],
style: {
display: "flex",
flexDirection: "column",
alignItems: "center",
textAlign: "center",
gap: themeSpacing(1),
},
},
emailField({
id: "login02-email",
fieldLabel: emailLabel,
placeholder: emailPlaceholder,
}),
passwordField({
id: "login02-password",
fieldLabel: passwordLabel,
forgotPasswordHref,
}),
submitButton(primaryButtonLabel),
dividerRow(dividerText),
// Upstream keeps the GitHub button and the sign-up line inside a single
// `<Field>` (flex-col gap-3 = 12px), so they sit closer together than
// the 28px field-group rhythm around them.
{
div: [
oauthButton({
brand: "github",
visibleLabel: githubButtonLabel,
accessibleLabel: githubButtonLabel,
onClick: onGithubClick,
}),
signUpLine({
promptText: signUpPrompt,
linkLabel: signUpLabel,
href: signUpHref,
}),
],
style: {
display: "flex",
flexDirection: "column",
gap: themeSpacing(3),
},
},
],
onSubmit: (event) => {
event.preventDefault();
const data = new FormData(event.target as HTMLFormElement);
onSubmit?.({
email: String(data.get("email") ?? ""),
password: String(data.get("password") ?? ""),
});
},
style: {
display: "flex",
flexDirection: "column",
// Upstream FieldGroup rhythm: gap-7 (28px) between the header, each
// Field, and the separator — not the form's own gap-6.
gap: themeSpacing(7),
width: "100%",
maxWidth: themeSpacing(80),
},
};
return {
div: [
{
div: [
brandRow,
{
div: [formBlock],
style: {
flex: "1 1 auto",
display: "flex",
alignItems: "center",
justifyContent: "center",
},
},
],
style: {
display: "flex",
flexDirection: "column",
gap: themeSpacing(4),
minWidth: "0",
// Upstream steps padding at the `md` breakpoint (p-6 -> md:p-10)
// rather than scaling it continuously.
padding: themeSpacing(6),
"@media (min-width: 48em)": { padding: themeSpacing(10) },
},
},
{
div: [
// Clean-room: no external photo is hotlinked by default — a caller-
// supplied coverImageSrc renders the photo, otherwise an understated
// theme-token dot-grid panel fills the column (see coverPanel()).
coverImageSrc
? coverImage({
src: coverImageSrc,
alt: coverImageAlt,
dimInDarkMode: dimCoverInDarkMode,
})
: coverPanel(),
],
style: {
minWidth: "0",
display: "flex",
[WIDE_SPLIT_MEDIA_QUERY]: { display: "none" },
},
} as DomphyElement<"div">,
],
style: {
display: "grid",
gridTemplateColumns: "1fr 1fr",
minHeight: "100svh",
[WIDE_SPLIT_MEDIA_QUERY]: { gridTemplateColumns: "1fr" },
},
};
}
export { Login02 };