Login03
A Auth block/component from shadcn/ui — clean-room reimplemented for Domphy (see methodology). Call Login03() with no arguments for a working demo, or edit the code below live.
Props
| Prop | Type | Description |
|---|---|---|
brandName | string | — |
heading | string | — |
subheading | string | — |
appleButtonLabel | string | — |
onAppleClick | () => void | — |
googleButtonLabel | string | — |
onGoogleClick | () => void | — |
dividerText | string | — |
emailLabel | string | — |
emailPlaceholder | string | — |
passwordLabel | string | — |
forgotPasswordHref | string | — |
primaryButtonLabel | string | — |
signUpPrompt | string | — |
signUpLabel | string | — |
signUpHref | string | — |
termsLabel | string | — |
termsHref | string | — |
privacyLabel | string | — |
privacyHref | string | — |
onSubmit | (values: { email: string; password: string }) => void | — |
Implementation notes
Muted-background page (dataTone shift-2 edge anchor), logo+wordmark row above card, OAuth-first button ordering (Apple/Google) above the divider and email/password form, legal Terms/Privacy footer below the card. Apple/Google glyphs are simplified hand-authored silhouettes, original geometry. Doctor diagnose(): 0 findings.
Status: ported · Reference: shadcn/ui original
// shadcn/ui "login-03" block — clean-room reimplementation.
//
// A single centered card on a muted-gray full-page background, offering
// Apple/Google sign-in above a classic email/password form, plus a legal
// disclaimer footer below the card. 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, Listener } from "@domphy/core";
import { themeColor, themeSize, themeSpacing } from "@domphy/theme";
import { card, heading, paragraph } from "@domphy/ui";
import { fixed } from "../../shared/typography.js";
import {
brandBadge,
dividerRow,
emailField,
legalFooter,
NARROW_CARD_WIDTH,
oauthButton,
passwordField,
signUpLine,
submitButton,
} from "./login01-05-shared.js";
/** Props for {@link Login03}. */
export interface Login03Props {
brandName?: string;
heading?: string;
subheading?: string;
appleButtonLabel?: string;
onAppleClick?: () => void;
googleButtonLabel?: string;
onGoogleClick?: () => void;
dividerText?: string;
emailLabel?: string;
emailPlaceholder?: string;
passwordLabel?: string;
forgotPasswordHref?: string;
primaryButtonLabel?: string;
signUpPrompt?: string;
signUpLabel?: string;
signUpHref?: string;
termsLabel?: string;
termsHref?: string;
privacyLabel?: string;
privacyHref?: string;
onSubmit?: (values: { email: string; password: string }) => void;
}
/**
* shadcn/ui "login-03" — muted-background page, OAuth-first card with a
* legal disclaimer footer. Call with no arguments for a fully working demo.
*/
function Login03(props: Login03Props = {}): DomphyElement<"div"> {
const {
brandName = "Acme Inc.",
heading: headingText = "Welcome back",
subheading = "Login with your Apple or Google account",
appleButtonLabel = "Login with Apple",
onAppleClick,
googleButtonLabel = "Login with Google",
onGoogleClick,
dividerText = "Or continue with",
emailLabel = "Email",
emailPlaceholder = "m@example.com",
passwordLabel = "Password",
forgotPasswordHref = "#",
primaryButtonLabel = "Login",
signUpPrompt = "Don't have an account?",
signUpLabel = "Sign up",
signUpHref = "#",
termsLabel = "Terms of Service",
termsHref = "#",
privacyLabel = "Privacy Policy",
privacyHref = "#",
onSubmit,
} = props;
const logoRow: DomphyElement<"a"> = {
a: [brandBadge(), brandName],
href: "#",
style: {
display: "flex",
alignItems: "center",
justifyContent: "center",
// Upstream logo link is `font-medium` (500) over the whole wordmark —
// not bold. `strong()` would render it at 700 and read as a heading.
fontWeight: fixed("500"),
gap: themeSpacing(2),
marginBlockEnd: themeSpacing(6),
// Reset the browser's default anchor styling (blue + underline) so the
// wordmark reads as plain foreground text, like login02's brand row.
color: "inherit",
textDecoration: fixed("none"),
},
};
const emailFieldRow = emailField({
id: "login03-email",
fieldLabel: emailLabel,
placeholder: emailPlaceholder,
});
const passwordFieldRow = passwordField({
id: "login03-password",
fieldLabel: passwordLabel,
forgotPasswordHref,
});
const cardElement: DomphyElement<"div"> = {
div: [
{ h2: headingText, $: [heading()], style: { textAlign: "center" } },
{
p: subheading,
$: [paragraph({ color: "neutral" })],
// `paragraph()` already sets `style.color` — the doctor tool inspects
// only this element's own inline style, not patch contributions, so
// it can't see that and flags a false positive here.
// (`_doctorDisable` is a doctor-only annotation absent from core's
// strict element type — build through an untyped literal + cast.)
_doctorDisable: "missing-color",
// Upstream CardDescription is `text-sm` (0.875rem), a step below base.
style: {
textAlign: "center",
fontSize: (listener: Listener) => themeSize(listener, "decrease-1"),
},
} as DomphyElement<"p">,
{
div: [
{
form: [
{
div: [
oauthButton({
brand: "apple",
visibleLabel: appleButtonLabel,
accessibleLabel: appleButtonLabel,
onClick: onAppleClick,
}),
oauthButton({
brand: "google",
visibleLabel: googleButtonLabel,
accessibleLabel: googleButtonLabel,
onClick: onGoogleClick,
}),
],
style: {
display: "flex",
flexDirection: "column",
gap: themeSpacing(3),
},
},
dividerRow(dividerText),
emailFieldRow,
passwordFieldRow,
{
div: [
submitButton(primaryButtonLabel),
signUpLine({
promptText: signUpPrompt,
linkLabel: signUpLabel,
href: signUpHref,
}),
],
style: {
display: "flex",
flexDirection: "column",
gap: themeSpacing(3),
},
},
],
onSubmit: (event: 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",
gap: themeSpacing(7),
},
} as DomphyElement<"form">,
],
},
],
$: [card({ color: "neutral" })],
// White card surface (upstream bg-card) so the card separates from the
// muted shift-1 page — card() paints an inherit-tone background; restate
// bg+color so the dataTone surface contract is self-contained on this host.
dataTone: "shift-0",
style: {
width: "100%",
maxWidth: NARROW_CARD_WIDTH,
backgroundColor: (listener: Listener) =>
themeColor(listener, "inherit", "neutral"),
color: (listener: Listener) => themeColor(listener, "shift-9", "neutral"),
},
};
return {
div: [
{
div: [
logoRow,
cardElement,
{
div: [
legalFooter({ termsLabel, termsHref, privacyLabel, privacyHref }),
],
style: {
marginBlockStart: themeSpacing(6),
width: "100%",
maxWidth: NARROW_CARD_WIDTH,
},
},
],
style: {
display: "flex",
flexDirection: "column",
alignItems: "center",
width: "100%",
},
},
],
dataTone: "shift-1",
style: {
display: "flex",
alignItems: "center",
justifyContent: "center",
minHeight: "100svh",
// 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) },
backgroundColor: (listener: Listener) =>
themeColor(listener, "inherit", "neutral"),
color: (listener: Listener) => themeColor(listener, "shift-9", "neutral"),
},
};
}
export { Login03 };