Domphy

signup02

A Auth block/component from shadcn/ui — clean-room reimplemented for Domphy (see methodology). Call signup02() with no arguments for a working demo, or edit the code below live.

Implementation notes

Two-column layout via pure CSS grid (1 col base, 1fr 1fr at @media(min-width)), left column uncarded form with logo row (centered on mobile, flex-start at lg via @media), vertically centered form block, GitHub outline button, 'Or continue with' divider() patch, right column full-bleed cover <img> (position, inset, object-fit) hidden below lg — all no-JS, matching the spec's 'purely via a CSS breakpoint' requirement. Same authFieldInput() local patch as signup01 for the same type-forcing reason. Fidelity gap: the dark-mode image dimming ('reduced brightness + grayscale filter') is approximated via @media (prefers-color-scheme: dark) rather than the app's explicit [data-theme=dark] attribute toggle, because Domphy's CSS-in-JS nesting (StyleList.addCSS) only supports SCSS-style &-prefixed self/descendant selectors, not an ancestor-attribute-before-& selector — there is no supported way to write [data-theme=dark] & as a style-object key. This is a reasonable best-effort approximation (OS-level dark preference) rather than a true app-theme-synced one; documented so a consumer can swap in a JS-driven filter toggle if exact data-theme sync is required.

Status: ported · Reference: shadcn/ui original

import type { DomphyElement, Listener, PartialElement } from "@domphy/core";
import { button, divider, heading, icon, label, link, paragraph, small, strong } from "@domphy/ui";
import { themeColor, themeDensity, themeFluidSpacing, themeSize, themeSpacing } from "@domphy/theme";

// Generic monochrome letter-badge glyphs — original, brand-neutral placeholders.
// Swap for official brand SVGs in production.
const GITHUB_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">H</text>' +
  "</svg>";

const LOGO_ICON =
  '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="1em" height="1em" fill="currentColor">' +
  '<path d="M12 3l8 16H4z" />' +
  "</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),
    },
  };
}

/** Small accent-colored square badge holding the logo glyph — edge-anchored tone surface. */
function logoMark(): DomphyElement<"span"> {
  return {
    span: [{ span: LOGO_ICON, $: [icon({ color: "inherit" })] }],
    dataTone: "shift-16",
    style: {
      display: "inline-flex",
      alignItems: "center",
      justifyContent: "center",
      width: (listener: Listener) => themeSpacing(themeDensity(listener) * 5),
      height: (listener: Listener) => themeSpacing(themeDensity(listener) * 5),
      borderRadius: (listener: Listener) => themeSpacing(themeDensity(listener) * 1),
      flexShrink: 0,
      backgroundColor: (listener: Listener) => themeColor(listener, "inherit", "primary"),
      color: (listener: Listener) => themeColor(listener, "shift-9", "primary"),
    },
  };
}

function logoRow(companyName: string, href: string): DomphyElement<"a"> {
  return {
    a: [logoMark(), { strong: companyName, $: [strong({ color: "neutral" })] }],
    href,
    $: [link({ color: "neutral" })],
    style: {
      display: "inline-flex",
      alignItems: "center",
      gap: (listener: Listener) => themeSpacing(themeDensity(listener) * 2),
    },
  };
}

/** Props for {@link signup02}. */
export interface Signup02Props {
  companyName?: string;
  logoHref?: string;
  coverImageSrc?: string;
  title?: string;
  subtitle?: string;
  fullNameLabel?: string;
  fullNamePlaceholder?: string;
  emailLabel?: string;
  emailPlaceholder?: string;
  emailCaption?: string;
  passwordLabel?: string;
  passwordCaption?: string;
  confirmPasswordLabel?: string;
  confirmPasswordCaption?: string;
  submitLabel?: string;
  showGithubButton?: boolean;
  githubButtonLabel?: string;
  signInPrompt?: string;
  signInLinkText?: string;
  signInHref?: string;
  onSubmit?: (event: SubmitEvent) => void;
}

/**
 * shadcn/ui "signup-02" — a two-column signup page: a branded, uncarded form
 * on the left and a full-bleed cover photograph on the right (hidden below
 * the `lg` breakpoint, no JS involved).
 */
function signup02(props: Signup02Props = {}): DomphyElement<"div"> {
  const {
    companyName = "Acme Inc.",
    logoHref = "#",
    coverImageSrc = "https://images.unsplash.com/photo-1502786129293-79981df4e689?w=1200&q=80",
    title = "Create your account",
    subtitle = "Fill in your details to get started",
    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",
    showGithubButton = true,
    githubButtonLabel = "Sign up with GitHub",
    signInPrompt = "Already have an account?",
    signInLinkText = "Sign in",
    signInHref = "#",
    onSubmit,
  } = props;

  const submitButton: DomphyElement<"button"> = {
    button: submitLabel,
    type: "submit",
    dataTone: "shift-17",
    $: [button({ color: "neutral" })],
    style: {
      width: "100%",
      backgroundColor: (listener: Listener) => themeColor(listener, "inherit", "neutral"),
      color: (listener: Listener) => themeColor(listener, "shift-9", "neutral"),
    },
  };

  const githubButton: DomphyElement<"button"> = {
    button: [
      { span: GITHUB_ICON, $: [icon({ color: "inherit" })] },
      { span: githubButtonLabel },
    ],
    type: "button",
    $: [button({ color: "neutral" })],
    style: { width: "100%" },
  };

  const formElement: DomphyElement<"form"> = {
    form: [
      field({ id: "signup02-name", labelText: fullNameLabel, placeholder: fullNamePlaceholder, autoComplete: "name" }),
      field({
        id: "signup02-email",
        labelText: emailLabel,
        type: "email",
        placeholder: emailPlaceholder,
        caption: emailCaption,
        autoComplete: "email",
      }),
      field({
        id: "signup02-password",
        labelText: passwordLabel,
        type: "password",
        caption: passwordCaption,
        autoComplete: "new-password",
        minLength: 8,
      }),
      field({
        id: "signup02-confirm-password",
        labelText: confirmPasswordLabel,
        type: "password",
        caption: confirmPasswordCaption,
        autoComplete: "new-password",
      }),
      submitButton,
      { div: "Or continue with", $: [divider({ color: "neutral" })] },
      showGithubButton ? githubButton : null,
    ],
    onSubmit: (event: Event) => {
      event.preventDefault();
      onSubmit?.(event as SubmitEvent);
    },
    style: {
      display: "flex",
      flexDirection: "column",
      gap: (listener: Listener) => themeSpacing(themeDensity(listener) * 4),
    },
  };

  const footerLine: DomphyElement<"small"> = {
    small: [
      `${signInPrompt} `,
      { a: signInLinkText, href: signInHref, $: [link({ color: "primary" })] },
    ],
    $: [small({ color: "neutral" })],
  };

  const contentBlock: DomphyElement<"div"> = {
    div: [{ h2: title, $: [heading()] }, { p: subtitle, $: [paragraph({ color: "neutral" })] }, formElement, footerLine],
    style: {
      width: "100%",
      maxWidth: themeSpacing(96),
      display: "flex",
      flexDirection: "column",
      gap: (listener: Listener) => themeSpacing(themeDensity(listener) * 3),
    },
  };

  const leftColumn: DomphyElement<"div"> = {
    div: [
      {
        div: [logoRow(companyName, logoHref)],
        style: {
          display: "flex",
          justifyContent: "center",
          paddingInline: themeFluidSpacing(4, 12),
          paddingBlock: themeSpacing(6),
          "@media(min-width:1024px)": { justifyContent: "flex-start" },
        },
      },
      {
        div: [contentBlock],
        style: {
          flex: "1",
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          paddingInline: themeFluidSpacing(4, 12),
          paddingBlock: themeFluidSpacing(4, 12),
        },
      },
    ],
    style: { display: "flex", flexDirection: "column", minHeight: "100vh" },
  };

  const rightColumn: DomphyElement<"div"> = {
    div: [
      {
        img: null,
        src: coverImageSrc,
        alt: "",
        style: {
          position: "absolute",
          inset: "0",
          width: "100%",
          height: "100%",
          objectFit: "cover",
          "@media (prefers-color-scheme: dark)": {
            filter: "brightness(0.45) grayscale(0.4)",
          },
        },
      },
    ],
    style: {
      position: "relative",
      display: "none",
      "@media(min-width:1024px)": { display: "block" },
    },
  };

  return {
    div: [leftColumn, rightColumn],
    style: {
      display: "grid",
      gridTemplateColumns: "1fr",
      minHeight: "100vh",
      "@media(min-width:1024px)": { gridTemplateColumns: "1fr 1fr" },
    },
  };
}

export { signup02 };

← Back to shadcn/ui catalog