Skip to content
Dethink Components

Good to see you again

Welcome back.

Your work, a little more in flow.

or use email

New here?
About this sign-in demo

Use sample credentials. Provider buttons simulate sign-in locally; nothing is sent or stored. No real account is created.

A local preview. Your real accounts stay untouched.

Recipe details

AuthIntermediateLoginSocialAutomationShaders

Automation social login

A split-layout sign-in page with a silk shader panel, Google and GitHub demos, email validation, password visibility, recovery, and retryable feedback.

Source

Copy the whole example or lift the component compositions into your own app. Recipes are showcase examples, not registry blocks.

examples/recipes/automation-login.tsx
"use client";

import Link from "next/link";
import { useAutomationReducedMotion } from "./automation-motion";

import { useEffect, useId, useRef, useState, type FormEvent } from "react";
import {
  ArrowLeft,
  ArrowRight,
  Check,
  Eye,
  EyeOff,
  LockKeyhole,
  Triangle,
} from "lucide-react";
import {
  Badge,
  Button,
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
  Field,
  FieldControl,
  FieldError,
  FieldLabel,
  Input,
  SilkFlowBackground,
  ShaderHeroText,
} from "@dethink/components";
import type { RecipePreviewProps } from "@/lib/recipe-presentation";
import "./automation-suite.css";

function GithubMark() {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="currentColor"
      aria-hidden="true"
      className="size-4"
    >
      <path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.58.11.79-.25.79-.55 0-.27-.01-1.17-.02-2.12-3.2.7-3.88-1.36-3.88-1.36-.52-1.33-1.28-1.68-1.28-1.68-1.04-.71.08-.7.08-.7 1.15.08 1.76 1.19 1.76 1.19 1.03 1.75 2.69 1.25 3.34.95.1-.74.4-1.25.72-1.54-2.55-.29-5.24-1.28-5.24-5.68 0-1.26.45-2.28 1.19-3.09-.12-.29-.52-1.46.11-3.05 0 0 .97-.31 3.17 1.18a11.1 11.1 0 0 1 5.78 0c2.2-1.49 3.17-1.18 3.17-1.18.63 1.59.23 2.76.11 3.05.74.81 1.19 1.83 1.19 3.09 0 4.41-2.69 5.38-5.26 5.66.41.35.77 1.04.77 2.1 0 1.52-.01 2.74-.01 3.11 0 .3.2.66.8.55A11.51 11.51 0 0 0 23.5 12C23.5 5.65 18.35.5 12 .5Z" />
    </svg>
  );
}

function RecoveryDialog() {
  const id = useId();
  const [sent, setSent] = useState(false);
  return (
    <Dialog onOpenChange={() => setSent(false)}>
      <DialogTrigger variant="link" size="sm" className="h-auto px-0 text-xs">
        Forgot your password?
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Let’s get you back in.</DialogTitle>
          <DialogDescription>
            This is a local recovery preview. No email will be sent.
          </DialogDescription>
        </DialogHeader>
        <form
          onSubmit={(event) => {
            event.preventDefault();
            // Portal events still bubble through the parent sign-in form.
            event.stopPropagation();
            setSent(true);
          }}
          className="space-y-5"
        >
          <Field id={id}>
            <FieldLabel>Email</FieldLabel>
            <FieldControl asChild>
              <Input
                name="recovery-email"
                type="email"
                autoComplete="email"
                required
                placeholder="you@company.com"
              />
            </FieldControl>
          </Field>
          <p role="status" className="text-muted-foreground text-sm">
            {sent
              ? "Recovery preview complete. A connected application would send a reset link after checking this request."
              : "Use a sample address to preview password recovery."}
          </p>
          <DialogFooter>
            <DialogClose variant="outline">Back to sign in</DialogClose>
            <Button type="submit">
              {sent ? "Preview again" : "Preview recovery"}
            </Button>
          </DialogFooter>
        </form>
      </DialogContent>
    </Dialog>
  );
}

function AccountDialog() {
  return (
    <Dialog>
      <DialogTrigger variant="link" size="sm" className="h-auto px-1 text-xs">
        Create an account
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Try your first workspace.</DialogTitle>
          <DialogDescription>
            This recipe previews sign-in without creating accounts. Choose
            Google, GitHub, or a sample email and password to explore the local
            success state.
          </DialogDescription>
        </DialogHeader>
        <DialogFooter>
          <DialogClose>Try the sign-in demo</DialogClose>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

export function AutomationLoginRecipe({
  presentation = "embedded",
}: RecipePreviewProps) {
  const id = useId();
  const reduceMotion = useAutomationReducedMotion();
  const [status, setStatus] = useState<
    "idle" | "pending" | "error" | "success"
  >("idle");
  const [provider, setProvider] = useState("Email");
  const [simulateFailure, setSimulateFailure] = useState(false);
  const [visible, setVisible] = useState(false);
  const [errors, setErrors] = useState<{ email?: string; password?: string }>(
    {},
  );
  const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
  const busy = useRef(false);
  const email = useRef<HTMLInputElement>(null);
  const password = useRef<HTMLInputElement>(null);
  const result = useRef<HTMLHeadingElement>(null);
  useEffect(
    () => () => {
      if (timer.current) clearTimeout(timer.current);
    },
    [],
  );
  useEffect(() => {
    if (status === "success") result.current?.focus();
  }, [status]);
  function signIn(method: string) {
    if (busy.current) return;
    busy.current = true;
    setProvider(method);
    setStatus("pending");
    setErrors({});
    if (password.current) password.current.value = "";
    const shouldFail = simulateFailure;
    timer.current = setTimeout(() => {
      busy.current = false;
      setStatus(shouldFail ? "error" : "success");
      setSimulateFailure(false);
    }, 850);
  }
  function submit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const next = {
      email: !email.current?.validity.valid
        ? "Enter a valid email address."
        : undefined,
      password: !password.current?.value
        ? "Enter a sample password."
        : undefined,
    };
    setErrors(next);
    if (next.email || next.password) {
      (next.email ? email : password).current?.focus();
      return;
    }
    signIn("Email");
  }
  const pending = status === "pending";
  return (
    <div
      data-recipe-surface="automation-login"
      className={`sc-automation grid min-h-[calc(100dvh-7rem)] lg:grid-cols-[0.95fr_1fr] ${presentation === "embedded" ? "border-border overflow-hidden rounded-xl border" : ""}`}
    >
      <aside
        aria-label="About Automation"
        className="automation-ink relative isolate hidden min-h-[780px] flex-col justify-between overflow-hidden p-12 lg:flex"
      >
        <div
          className="pointer-events-none absolute inset-0 -z-10"
          aria-hidden="true"
        >
          <SilkFlowBackground
            animate={!reduceMotion}
            speed="fast"
            intensity="bold"
            seed={4}
            className="h-full w-full"
          />
          <div className="from-background/10 via-background/30 to-background/80 absolute inset-0 bg-gradient-to-b" />
        </div>
        <Link
          href="/recipes/automation-landing"
          className="flex items-center gap-2 text-sm font-semibold"
        >
          <Triangle aria-hidden="true" className="text-primary size-5" />
          AUTOMATION<span className="text-primary">®</span>
        </Link>
        <div className="pt-32 pb-24">
          <Badge
            variant="soft"
            className="border-primary/30 bg-background/80 text-primary mb-7 border"
          >
            Make space for what’s next
          </Badge>
          {/* ShaderHeroText supplies semantic heading content through text. */}
          {/* eslint-disable-next-line jsx-a11y/heading-has-content */}
          <ShaderHeroText
            as="h2"
            text="Your next great workflow starts here."
            animation="particle-follow"
            intensity={0.85}
            reducedMotion={reduceMotion ? "always" : "user"}
            className="max-w-sm font-serif text-[3.6rem] leading-[1.02] tracking-[-0.045em]"
          />
          <p className="text-muted-foreground mt-7 max-w-xs text-sm leading-7">
            Same tools. Less busywork.
            <br />A more human way to work.
          </p>
        </div>
        <div className="border-border border-t pt-6">
          <p className="text-muted-foreground text-xs leading-6">
            A little less repetition.
            <br />A little more possibility.
          </p>
          <p className="text-primary mt-4 text-[9px] tracking-[0.22em] uppercase">
            Build · Automate · Move forward
          </p>
        </div>
      </aside>
      <section className="flex min-w-0 flex-col px-6 py-7 sm:px-12 lg:px-14">
        <div className="flex items-center justify-between gap-4 text-xs">
          <Link
            href="/recipes/automation-landing"
            className="text-muted-foreground inline-flex items-center gap-2"
          >
            <ArrowLeft aria-hidden="true" className="size-3.5" />
            Back to Automation
          </Link>
          <Badge variant="soft">Demo</Badge>
        </div>
        <div className="mx-auto flex w-full max-w-[350px] flex-1 flex-col justify-center py-14 lg:py-16">
          {status === "success" ? (
            <div className="space-y-6">
              <div className="bg-primary/10 text-primary flex size-12 items-center justify-center rounded-full">
                <Check aria-hidden="true" className="size-6" />
              </div>
              <h1
                ref={result}
                tabIndex={-1}
                className="text-3xl font-medium tracking-tight outline-none"
              >
                You’re ready to explore.
              </h1>
              <p className="text-muted-foreground text-sm leading-6">
                {provider} sign-in demo complete. No account was created and no
                credentials were sent.
              </p>
              <Button asChild className="w-full">
                <Link href="/recipes/automation-landing">
                  Explore the workflows{" "}
                  <ArrowRight aria-hidden="true" className="size-4" />
                </Link>
              </Button>
              <Button
                variant="outline"
                className="w-full"
                onClick={() => {
                  setStatus("idle");
                  setVisible(false);
                }}
              >
                Try another sign-in method
              </Button>
            </div>
          ) : (
            <>
              <div className="mb-8">
                <p className="text-primary mb-4 text-[10px] font-medium tracking-[0.19em] uppercase">
                  Good to see you again
                </p>
                <h1 className="text-4xl font-medium tracking-[-0.04em]">
                  Welcome back.
                </h1>
                <p className="text-muted-foreground mt-3 text-sm">
                  Your work, a little more in flow.
                </p>
              </div>
              <div className="grid gap-3">
                <Button
                  variant="outline"
                  size="lg"
                  disabled={pending}
                  onClick={() => signIn("Google")}
                  className="w-full text-sm"
                >
                  <span aria-hidden="true" className="text-lg font-bold">
                    G
                  </span>
                  Continue with Google
                </Button>
                <Button
                  variant="outline"
                  size="lg"
                  disabled={pending}
                  onClick={() => signIn("GitHub")}
                  className="w-full text-sm"
                >
                  <GithubMark />
                  Continue with GitHub
                </Button>
              </div>
              <div className="text-muted-foreground my-7 flex items-center gap-4 text-[11px]">
                <span className="bg-border h-px flex-1" />
                or use email
                <span className="bg-border h-px flex-1" />
              </div>
              <form
                noValidate
                onSubmit={submit}
                aria-label="Email sign in"
                aria-busy={pending}
                className="space-y-5"
              >
                <Field id={`${id}-email`} invalid={Boolean(errors.email)}>
                  <FieldLabel>Email</FieldLabel>
                  <FieldControl asChild>
                    <Input
                      ref={email}
                      name="email"
                      type="email"
                      autoComplete="username"
                      required
                      placeholder="you@company.com"
                      disabled={pending}
                      onChange={() =>
                        setErrors((current) => ({
                          ...current,
                          email: undefined,
                        }))
                      }
                      className="h-11"
                    />
                  </FieldControl>
                  {errors.email && <FieldError>{errors.email}</FieldError>}
                </Field>
                <Field id={`${id}-password`} invalid={Boolean(errors.password)}>
                  <FieldLabel>Password</FieldLabel>
                  <div className="relative">
                    <FieldControl asChild>
                      <Input
                        ref={password}
                        name="password"
                        type={visible ? "text" : "password"}
                        autoComplete="current-password"
                        required
                        placeholder="Enter a sample password"
                        disabled={pending}
                        onChange={() =>
                          setErrors((current) => ({
                            ...current,
                            password: undefined,
                          }))
                        }
                        className="h-11 pr-12"
                      />
                    </FieldControl>
                    <Button
                      type="button"
                      variant="ghost"
                      size="icon"
                      disabled={pending}
                      aria-label={visible ? "Hide password" : "Show password"}
                      aria-pressed={visible}
                      onClick={() => setVisible(!visible)}
                      className="absolute top-0.5 right-1 size-10"
                    >
                      {visible ? (
                        <EyeOff aria-hidden="true" className="size-4" />
                      ) : (
                        <Eye aria-hidden="true" className="size-4" />
                      )}
                    </Button>
                  </div>
                  {errors.password && (
                    <FieldError>{errors.password}</FieldError>
                  )}
                </Field>
                <div className="flex justify-end">
                  <RecoveryDialog />
                </div>
                <Button
                  type="submit"
                  size="lg"
                  loading={pending && provider === "Email"}
                  disabled={pending}
                  className="w-full text-sm"
                >
                  Sign in <ArrowRight aria-hidden="true" className="size-4" />
                </Button>
              </form>
              <div className="mt-4 min-h-6 text-xs leading-5">
                <p role="status">
                  {pending ? `Previewing ${provider} sign-in…` : ""}
                </p>
                {status === "error" && (
                  <div
                    role="alert"
                    className="border-border bg-muted rounded-lg border p-3"
                  >
                    <p>
                      The {provider} demo could not complete. This was a
                      simulated error.
                    </p>
                    <Button
                      variant="link"
                      size="sm"
                      className="mt-1 px-0"
                      onClick={() => signIn(provider)}
                    >
                      Retry {provider} demo
                    </Button>
                  </div>
                )}
              </div>
              <div className="text-muted-foreground mt-4 flex items-center justify-center gap-1 text-xs">
                New here? <AccountDialog />
              </div>
              <details className="border-border text-muted-foreground mt-8 rounded-lg border p-3 text-xs">
                <summary className="cursor-pointer">
                  About this sign-in demo
                </summary>
                <p className="mt-3 leading-5">
                  Use sample credentials. Provider buttons simulate sign-in
                  locally; nothing is sent or stored. No real account is
                  created.
                </p>
                <label className="mt-3 flex cursor-pointer items-center gap-2">
                  <input
                    type="checkbox"
                    checked={simulateFailure}
                    disabled={pending}
                    onChange={(event) =>
                      setSimulateFailure(event.target.checked)
                    }
                    className="accent-primary size-4"
                  />
                  Simulate a failed sign-in once
                </label>
              </details>
            </>
          )}
        </div>
        <p className="text-muted-foreground flex items-center justify-center gap-2 text-center text-[10px] leading-5">
          <LockKeyhole aria-hidden="true" className="size-3" />A local preview.
          Your real accounts stay untouched.
        </p>
      </section>
    </div>
  );
}