signup01
A Auth block/component from shadcn/ui — clean-room reimplemented for Domphy (see methodology). Call signup01() with no arguments for a working demo, or edit the code below live.
Implementation notes
Minimal single-card signup form: card() patch (@domphy/ui) auto-places h2/p/div/footer into title/desc/content/footer grid areas; Full Name, Email(+caption), Password(+caption, minlength=8), Confirm Password(+caption) fields; solid dark submit button (dataTone shift-17 edge anchor + button({color:'neutral'})) directly followed by an outline Google button with NO divider between them, matching the spec's researchNote. Reactive loading (spinner + aria-busy + disabled) and error (alert banner) props wired via toState. Deviation: does NOT reuse @domphy/ui's inputText()/inputPassword() patches for the actual <input> elements — inputText() forces type='text' via an unconditional _onSchedule hook (confirmed by reading packages/ui/src/patches/inputText.ts and tracing ElementNode's constructor order), which would silently unmask password fields and break type=email semantics; inputPassword() is a div-wrapper that builds its own internal <input> imperatively with no id/name/required/autocomplete passthrough. Built a small local authFieldInput() patch replicating inputText()'s exact visual formula (theme tokens only) instead, preserving correct native type/required/minlength/autocomplete attributes as the spec's behavior section demands. Google glyph is an original brand-neutral letter-badge SVG (not a reproduction of Google's official mark, since a raw multicolor brand logo would also violate the no-raw-hex-color doctor rule).
Status: ported · Reference: shadcn/ui original
import type {
DomphyElement,
Listener,
PartialElement,
ValueOrState,
} from "@domphy/core";
import { toState } from "@domphy/core";
import {
alert,
button,
card,
heading,
icon,
label,
link,
paragraph,
small,
spinner,
} from "@domphy/ui";
import {
themeColor,
themeDensity,
themeFluidSpacing,
themeSize,
themeSpacing,
} from "@domphy/theme";
// Generic monochrome "G" glyph — an original, brand-neutral placeholder for
// the Google provider button. Swap for an official brand SVG in production.
const GOOGLE_ICON =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="1em" height="1em" ' +
'fill="none" stroke="currentColor" stroke-width="1.5">' +
'<circle cx="12" cy="12" r="9.25" />' +
'<text x="12" y="16" text-anchor="middle" font-size="10" stroke="none" fill="currentColor">G</text>' +
"</svg>";
/**
* Visual formula for a bounded text-like `<input>`, matching @domphy/ui's
* `inputText()` patch. Written as a local patch instead of reusing
* `inputText()` directly because that patch forces `type="text"` via
* `_onSchedule`, which would silently unmask `type="password"`/`"email"`
* fields — see the port's fidelity notes.
*/
function authFieldInput(): PartialElement {
return {
style: {
fontFamily: "inherit",
lineHeight: "inherit",
width: "100%",
boxSizing: "border-box",
paddingInline: (listener: Listener) =>
themeSpacing(themeDensity(listener) * 3),
paddingBlock: (listener: Listener) =>
themeSpacing(themeDensity(listener) * 1),
borderRadius: (listener: Listener) =>
themeSpacing(themeDensity(listener) * 1),
fontSize: (listener: Listener) => themeSize(listener, "inherit"),
border: "none",
outlineOffset: "-1px",
outline: (listener: Listener) =>
`1px solid ${themeColor(listener, "shift-4", "neutral")}`,
color: (listener: Listener) => themeColor(listener, "shift-9", "neutral"),
backgroundColor: (listener: Listener) =>
themeColor(listener, "inherit", "neutral"),
"&::placeholder": {
color: (listener: Listener) => themeColor(listener, "shift-7", "neutral"),
},
"&:hover:not([disabled]), &:focus-visible": {
outline: (listener: Listener) =>
`${themeSpacing(0.5)} solid ${themeColor(listener, "shift-6", "primary")}`,
},
"&[disabled]": {
opacity: 0.7,
cursor: "not-allowed",
backgroundColor: (listener: Listener) =>
themeColor(listener, "shift-2", "neutral"),
},
},
};
}
interface FieldConfig {
id: string;
labelText: string;
type?: "text" | "email" | "password";
placeholder?: string;
caption?: string;
autoComplete?: string;
minLength?: number;
}
function field(config: FieldConfig): DomphyElement<"div"> {
const {
id,
labelText,
type = "text",
placeholder,
caption,
autoComplete,
minLength,
} = config;
return {
div: [
{ label: labelText, for: id, $: [label()] },
{
input: null,
id,
name: id,
type,
placeholder,
required: true,
autocomplete: autoComplete,
minlength: minLength,
$: [authFieldInput()],
},
caption ? { small: caption, $: [small({ color: "neutral" })] } : null,
],
style: {
display: "flex",
flexDirection: "column",
gap: (listener: Listener) => themeSpacing(themeDensity(listener) * 1),
},
};
}
/** Props for {@link signup01}. */
export interface Signup01Props {
title?: string;
subtitle?: string;
fullNameLabel?: string;
fullNamePlaceholder?: string;
emailLabel?: string;
emailPlaceholder?: string;
emailCaption?: string;
passwordLabel?: string;
passwordCaption?: string;
confirmPasswordLabel?: string;
confirmPasswordCaption?: string;
submitLabel?: string;
showGoogleButton?: boolean;
googleButtonLabel?: string;
signInPrompt?: string;
signInLinkText?: string;
signInHref?: string;
/** Reactive busy state — disables the submit button and shows a spinner. */
loading?: ValueOrState<boolean>;
/** Reactive error message — renders an inline alert above the submit button when set. */
error?: ValueOrState<string | null>;
onSubmit?: (event: SubmitEvent) => void;
}
/**
* shadcn/ui "signup-01" — a minimal single-card signup form centered in the
* viewport: Full Name, Email, Password, Confirm Password, a solid submit
* button and an optional outline Google button.
*/
function signup01(props: Signup01Props = {}): DomphyElement<"div"> {
const {
title = "Create an account",
subtitle = "Enter your details below to create your account",
fullNameLabel = "Full Name",
fullNamePlaceholder = "John Doe",
emailLabel = "Email",
emailPlaceholder = "m@example.com",
emailCaption = "We'll never share your email with anyone else.",
passwordLabel = "Password",
passwordCaption = "Must be at least 8 characters long.",
confirmPasswordLabel = "Confirm Password",
confirmPasswordCaption = "Please re-enter your password.",
submitLabel = "Create Account",
showGoogleButton = true,
googleButtonLabel = "Sign up with Google",
signInPrompt = "Already have an account?",
signInLinkText = "Sign in",
signInHref = "#",
loading = false,
error = null,
onSubmit,
} = props;
const loadingState = toState(loading, "loading");
const errorState = toState(error, "error");
const submitButton: DomphyElement<"button"> = {
button: (listener: Listener) =>
loadingState.get(listener)
? [{ span: null, $: [spinner({ color: "neutral" })] }, submitLabel]
: submitLabel,
type: "submit",
dataTone: "shift-17",
disabled: (listener: Listener) => loadingState.get(listener),
ariaBusy: (listener: Listener) => (loadingState.get(listener) ? "true" : "false"),
$: [button({ color: "neutral" })],
style: {
width: "100%",
backgroundColor: (listener: Listener) => themeColor(listener, "inherit", "neutral"),
color: (listener: Listener) => themeColor(listener, "shift-9", "neutral"),
},
};
const googleButton: DomphyElement<"button"> = {
button: [
{ span: GOOGLE_ICON, $: [icon({ color: "inherit" })] },
{ span: googleButtonLabel },
],
type: "button",
$: [button({ color: "neutral" })],
style: { width: "100%" },
};
const errorBanner: DomphyElement<"div"> = {
div: (listener: Listener) => errorState.get(listener) ?? "",
hidden: (listener: Listener) => !errorState.get(listener),
$: [alert({ color: "error" })],
};
const cardBody: DomphyElement<"div"> = {
div: [
{
form: [
field({
id: "signup01-name",
labelText: fullNameLabel,
placeholder: fullNamePlaceholder,
autoComplete: "name",
}),
field({
id: "signup01-email",
labelText: emailLabel,
type: "email",
placeholder: emailPlaceholder,
caption: emailCaption,
autoComplete: "email",
}),
field({
id: "signup01-password",
labelText: passwordLabel,
type: "password",
caption: passwordCaption,
autoComplete: "new-password",
minLength: 8,
}),
field({
id: "signup01-confirm-password",
labelText: confirmPasswordLabel,
type: "password",
caption: confirmPasswordCaption,
autoComplete: "new-password",
}),
errorBanner,
submitButton,
showGoogleButton ? googleButton : null,
],
onSubmit: (event: Event) => {
event.preventDefault();
onSubmit?.(event as SubmitEvent);
},
style: {
display: "flex",
flexDirection: "column",
gap: (listener: Listener) => themeSpacing(themeDensity(listener) * 4),
},
},
],
};
const cardFooter: DomphyElement<"footer"> = {
footer: [
{
small: [
`${signInPrompt} `,
{ a: signInLinkText, href: signInHref, $: [link({ color: "primary" })] },
],
$: [small({ color: "neutral" })],
},
],
};
const cardElement: DomphyElement<"div"> = {
div: [{ h2: title, $: [heading()] }, { p: subtitle, $: [paragraph({ color: "neutral" })] }, cardBody, cardFooter],
style: {
width: "100%",
maxWidth: themeSpacing(96),
},
$: [card({ color: "neutral" })],
};
return {
div: [cardElement],
style: {
display: "flex",
alignItems: "center",
justifyContent: "center",
minHeight: "100vh",
paddingInline: themeFluidSpacing(4, 12),
paddingBlock: themeFluidSpacing(4, 12),
},
};
}
export { signup01 };