# AuthLayout

> La pantalla de entrar, registrarse, recuperar la clave o esperar una aprobación, como la de iCloud: columna de 400, marca, título grande, el formulario en una card y los links abajo.

```tsx
import { AuthContent, AuthDescription, AuthDivider, AuthError, … } from "sebs7n-ui/auth-layout"
```

Sin `"use client"`: sirve en un Server Component.

## Ejemplos

### Iniciar sesión, sobre el wallpaper

La columna de 400: la marca, el `<h1>` y una línea arriba, el formulario en la card y los links abajo. Con `ambient` la card pasa al material translúcido; los campos siguen opacos. Probá entrar con cualquier contraseña: el error que no es de un campo va en `AuthError`, arriba.

```tsx
import { AuthContent, AuthDescription, AuthError, AuthFooter, AuthHeader, AuthLayout, AuthTitle } from "sebs7n-ui/auth-layout"
import { Button } from "sebs7n-ui/button"
import { Field, FieldError, FieldLabel } from "sebs7n-ui/field"
import { Form } from "sebs7n-ui/form"
import { Input } from "sebs7n-ui/input"
import { PasswordInput } from "sebs7n-ui/password-input"
import { linkVariants } from "sebs7n-ui/variants/link"
import { useState } from "react"

function SignIn() {
  const [error, setError] = useState(false)
  return (
    <AuthLayout ambient as="div" className={BOX}>
      <AuthHeader brand={<Brand />}>
        <AuthTitle>Iniciar sesión</AuthTitle>
        <AuthDescription>Entrá para ver tus facturas y cobros.</AuthDescription>
      </AuthHeader>
      <AuthContent>
        <Form className="gap-4" onFormSubmit={() => setError(true)}>
          {error && <AuthError>Email o contraseña incorrectos.</AuthError>}
          <Field name="email">
            <FieldLabel>Email</FieldLabel>
            <Input autoComplete="username" required type="email" />
            <FieldError match="valueMissing">Falta el email</FieldError>
          </Field>
          <Field name="password">
            <div className="flex items-center justify-between">
              <FieldLabel>Contraseña</FieldLabel>
              <a className={linkVariants({ variant: "subtle" })} href="#ejemplos">
                ¿La olvidaste?
              </a>
            </div>
            <PasswordInput autoComplete="current-password" required />
            <FieldError match="valueMissing">Falta la contraseña</FieldError>
          </Field>
          <Button className="w-full" type="submit">
            Iniciar sesión
          </Button>
        </Form>
      </AuthContent>
      <AuthFooter>
        <p>
          ¿No tenés cuenta?{" "}
          <a className={link} href="#ejemplos">
            Registrate
          </a>
        </p>
      </AuthFooter>
    </AuthLayout>
  )
}
```

### Crear una cuenta

Un aviso que sigue siendo verdad en un `Alert`, arriba de todo; el proveedor (`secondary`: el acento es del envío) con el «o» en el medio, y los términos en una casilla obligatoria de verdad: al enviar sin tildar, `Form` lleva el foco ahí y dice por qué. La barra de arriba (`bar`) queda fuera del `main`.

```tsx
import { Alert, AlertDescription } from "sebs7n-ui/alert"
import { ArrowLeftIcon } from "lucide-react"
import { AuthContent, AuthDescription, AuthDivider, AuthFooter, AuthHeader, AuthLayout, AuthProviders, AuthTitle } from "sebs7n-ui/auth-layout"
import { Button } from "sebs7n-ui/button"
import { Checkbox } from "sebs7n-ui/checkbox"
import { Field, FieldError, FieldLabel } from "sebs7n-ui/field"
import { Form } from "sebs7n-ui/form"
import { Input } from "sebs7n-ui/input"
import { PasswordInput } from "sebs7n-ui/password-input"
import { buttonVariants } from "sebs7n-ui/variants/button"

function SignUp() {
  return (
    <AuthLayout
      as="div"
      bar={
        <a className={buttonVariants({ variant: "plain", size: "sm" })} href="#ejemplos">
          <ArrowLeftIcon />
          Volver al inicio
        </a>
      }
      className={BOX}
    >
      <AuthHeader brand={<Brand />}>
        <AuthTitle>Crear una cuenta</AuthTitle>
        <AuthDescription>Facturá y cobrá desde un solo lugar.</AuthDescription>
      </AuthHeader>
      <AuthContent>
        <Alert>
          <AlertDescription>Un administrador de tu estudio aprueba las cuentas nuevas antes del primer ingreso.</AlertDescription>
        </Alert>
        <AuthProviders>
          <Button type="button" variant="secondary">
            Continuar con Google
          </Button>
        </AuthProviders>
        <AuthDivider />
        <Form className="gap-4">
          <Field name="name">
            <FieldLabel>Nombre y apellido</FieldLabel>
            <Input autoComplete="name" required />
            <FieldError match="valueMissing">Falta tu nombre</FieldError>
          </Field>
          <Field name="email">
            <FieldLabel>Email</FieldLabel>
            <Input autoComplete="email" required type="email" />
            <FieldError match="valueMissing">Falta el email</FieldError>
          </Field>
          <Field name="password">
            <FieldLabel>Contraseña</FieldLabel>
            <PasswordInput autoComplete="new-password" minLength={8} required strength />
            <FieldError match="valueMissing">Falta la contraseña</FieldError>
            <FieldError match="tooShort">Tiene que tener 8 caracteres o más</FieldError>
          </Field>
          <Field name="terms">
            <div className="flex items-start gap-3">
              <Checkbox required />
              <FieldLabel>
                <span>
                  Acepto los{" "}
                  <a className={link} href="#ejemplos">
                    términos
                  </a>{" "}
                  y la{" "}
                  <a className={link} href="#ejemplos">
                    política de privacidad
                  </a>
                </span>
              </FieldLabel>
            </div>
            <FieldError match="valueMissing">Para seguir, aceptá los términos</FieldError>
          </Field>
          <Button className="w-full" type="submit">
            Crear cuenta
          </Button>
        </Form>
      </AuthContent>
      <AuthFooter>
        <p>
          ¿Ya tenés cuenta?{" "}
          <a className={link} href="#ejemplos">
            Iniciá sesión
          </a>
        </p>
        <p className="text-footnote">Al continuar con Google aceptás los mismos términos.</p>
      </AuthFooter>
    </AuthLayout>
  )
}
```

### Recuperar la contraseña

Un campo y el envío. Al mandarlo, `AuthStatus` queda en lugar del formulario, con su propio `<h1>` y `role="status"`: se anuncia sin cambiar de página.

```tsx
import { AuthContent, AuthDescription, AuthFooter, AuthHeader, AuthLayout, AuthStatus, AuthTitle } from "sebs7n-ui/auth-layout"
import { Button } from "sebs7n-ui/button"
import { Field, FieldError, FieldLabel } from "sebs7n-ui/field"
import { Form } from "sebs7n-ui/form"
import { Input } from "sebs7n-ui/input"
import { MailCheckIcon } from "lucide-react"
import { linkVariants } from "sebs7n-ui/variants/link"
import { useState } from "react"

function Recover() {
  const [sent, setSent] = useState<string | null>(null)
  return (
    <AuthLayout as="div" className={BOX}>
      {sent ? (
        <>
          <AuthHeader brand={<Brand />} />
          <AuthContent>
            <AuthStatus
              action={
                <Button onClick={() => setSent(null)} variant="secondary">
                  Usar otro email
                </Button>
              }
              description={`Si ${sent} tiene una cuenta, te llega un enlace para elegir una contraseña nueva. Vence en una hora.`}
              icon={<MailCheckIcon />}
              title="Revisá tu email"
            />
          </AuthContent>
        </>
      ) : (
        <>
          <AuthHeader brand={<Brand />}>
            <AuthTitle>Recuperar la contraseña</AuthTitle>
            <AuthDescription>Te mandamos un enlace para elegir una nueva.</AuthDescription>
          </AuthHeader>
          <AuthContent>
            <Form className="gap-4" onFormSubmit={(values) => setSent(String(values.email))}>
              <Field name="email">
                <FieldLabel>Email</FieldLabel>
                <Input autoComplete="email" required type="email" />
                <FieldError match="valueMissing">Falta el email</FieldError>
              </Field>
              <Button className="w-full" type="submit">
                Enviar enlace
              </Button>
            </Form>
          </AuthContent>
        </>
      )}
      <AuthFooter>
        <a className={linkVariants({ variant: "subtle" })} href="#ejemplos">
          Volver a iniciar sesión
        </a>
      </AuthFooter>
    </AuthLayout>
  )
}
```

### Cuenta pendiente de aprobación

La cuenta existe pero todavía no entra: `AuthStatus` con `tone="warning"`, qué pasa y con quién hablar. La acción que sigue va a todo el ancho; salir, abajo.

```tsx
import { AuthContent, AuthFooter, AuthHeader, AuthLayout, AuthStatus } from "sebs7n-ui/auth-layout"
import { ClockIcon } from "lucide-react"
import { buttonVariants } from "sebs7n-ui/variants/button"
import { linkVariants } from "sebs7n-ui/variants/link"

function Pending() {
  return (
    <AuthLayout as="div" className={BOX}>
      <AuthHeader brand={<Brand />} />
      <AuthContent>
        <AuthStatus
          action={
            <a className={buttonVariants({ variant: "secondary" })} href="#ejemplos">
              Escribirle al administrador
            </a>
          }
          description="Un administrador de tu estudio revisa las cuentas nuevas. Te avisamos por email cuando puedas entrar."
          icon={<ClockIcon />}
          title="Tu cuenta espera la aprobación"
          tone="warning"
        />
      </AuthContent>
      <AuthFooter>
        <button className={linkVariants({ variant: "subtle" })} type="button">
          Cerrar sesión
        </button>
      </AuthFooter>
    </AuthLayout>
  )
}
```

## Props

### AuthContent

Hereda las props de `<div>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `autoFocus` | `boolean` | `false` | Enfoca el primer campo al montar (no el botón del proveedor). Para la pantalla que es solo entrar; en un panel adentro de otra página, no. |
| `variant` | `"plain" \| "card"` | `"card"` | `card` (default): la card de iCloud, que sobre el wallpaper pasa al material translúcido. `plain`: sin superficie, para una pantalla que ya es una hoja o para un panel adentro de otra card (el de entrar en medio de una compra). |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### AuthDescription

Hereda las props de `<p>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### AuthDivider

Hereda las props de `<div>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `labels` | `Partial<NonNullable<Labels["auth"]>>` | — | Textos: `or` («o»). Le gana al `LabelsProvider` (`auth.or`). |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### AuthError

Hereda las props de `<div>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `title` | `React.ReactNode` | — | Una línea en negrita arriba del mensaje. Casi siempre alcanza con el mensaje solo. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### AuthFooter

Hereda las props de `<div>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### AuthHeader

Hereda las props de `<div>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `brand` | `React.ReactNode` | — | La marca de la app, arriba del título: un logo o un ícono de 48. Es decorativa (el título ya dice dónde se está). Si es un link al inicio, pasalo con su nombre: `<a aria-label="Inicio">`. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### AuthLayout

Hereda las props de `<div>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `ambient` | `boolean` | `false` | El wallpaper de la home de iCloud detrás (`bg-ambient`), con la card en el material translúcido; los campos siguen opacos. Liso por defecto, como una app por dentro. |
| `as` | `"div" \| "main"` | `"main"` | `main` por defecto; `div` si el layout de la app ya pone su `<main>`. |
| `bar` | `React.ReactNode` | — | La barra de arriba, fuera del `main`: «Volver al inicio» a la izquierda, el tema a la derecha. |
| `mainId` | `string` | — | El `id` del `main`, para el «Ir al contenido» de la app. |
| `className` | `string` | — | Clases de la raíz (la que pinta la página). En una caja, `min-h-0`. |

### AuthProviders

Hereda las props de `<div>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### AuthStatus

Hereda las props de `<div>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `title` * | `React.ReactNode` | — | Qué pasó, en una línea: «Revisá tu email». Sale como el heading que diga `titleAs`. |
| `action` | `React.ReactNode` | — | El botón o link para seguir: «Volver a enviar», «Ir al inicio». Varios, uno abajo del otro. |
| `description` | `React.ReactNode` | — | Qué hacer ahora y cuánto tarda. |
| `icon` | `React.ReactNode` | — | El ícono de arriba, en un círculo gris. Decorativo (`aria-hidden`); su color sale de `tone`. |
| `titleAs` | `"h1" \| "h2" \| "h3"` | `"h1"` | h1 por defecto: reemplaza al título de la pantalla («Revisá tu email»). h2 si hay un `AuthTitle` arriba. |
| `tone` | `"neutral" \| "brand" \| "success" \| "warning" \| "error"` | `"brand"` | El color del ícono. `brand` por defecto. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

`*` obligatoria.

### AuthTitle

Hereda las props de `<h1>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

## Teclado

| Tecla | Qué hace |
|---|---|
| Tab | Recorre la barra (si hay), los proveedores, el formulario y los links del pie, en ese orden. |
| — | Con `autoFocus` en `AuthContent`, el foco arranca en el primer campo y no en «Continuar con Google». |

## Accesibilidad

- **Un solo `<h1>`**: `AuthTitle`, o el título de `AuthStatus` cuando reemplaza al formulario (h1 por defecto; `titleAs="h2"` si queda un `AuthTitle` arriba).
- `AuthLayout` pone el `<main>` (`mainId` para el «Ir al contenido» de la app); la `bar` queda afuera. Si el layout de la app ya tiene su `<main>`, `as="div"`.
- **Errores**: los de un campo, en su `FieldError` con el `errors` de `Form`, que enfoca el primero. El que no es de ningún campo («Email o contraseña incorrectos») va en `AuthError`, arriba de los campos: `role="alert"`, ícono además del color y `tabIndex=-1` para poder llevarle el foco. Con un `id`, el botón de enviar puede nombrarlo en `aria-describedby`.
- `AuthStatus` es `role="status"`: si aparece en lugar del formulario sin cambiar de página («Revisá tu email»), se anuncia sin cortar. El ícono es decorativo; el tono no es el único dato, lo dice el título.
- `AuthDivider`: las dos líneas son `aria-hidden` y el «o» se lee, para que se entienda que lo que sigue es otra forma de entrar. Traducible con `labels.auth.or`.
- `autoFocus` es opt-in: en la pantalla que es solo entrar ayuda; en un panel dentro de otra página (entrar en medio de una compra) le saca el lugar al lector.
- Sin `"use client"`: toda la pantalla sirve en un Server Component (el texto del separador y el foco inicial son dos piezas de cliente adentro).

## Reglas de uso

- **El título va afuera de la card** (`AuthHeader`): es el de la página y se lee igual con `AuthContent variant="plain"`.
- Proveedores arriba del formulario, con `AuthDivider` en el medio, y `variant="secondary"`: el acento sólido es del envío. Sin logos de marca de terceros en el paquete: el ícono lo pone la app si quiere.
- Un aviso que sigue siendo verdad («Un administrador aprueba las cuentas nuevas») es un `Alert` al principio de `AuthContent`, antes de los proveedores.
- Los términos: una casilla obligatoria de verdad (`Checkbox required` en un `Field`), no un botón apagado; al enviar sin tildar, `Form` lleva el foco ahí. El texto legal pasivo va en `AuthFooter`, en `text-footnote`.
- `AuthFooter`: «¿No tenés cuenta? Registrate» con `linkVariants({ variant: "inline" })`; «Volver a iniciar sesión» o «Cerrar sesión» con `subtle`. Varios links legales, en un `<nav aria-label="Legales">`.
- Arriba en el teléfono, centrada en alto desde `sm`. `ambient` pinta el wallpaper de iCloud y pasa la card al material translúcido; los campos siguen opacos.
- Solo por subpath (`sebs7n-ui/auth-layout`).

## Relacionados

[form](/docs/components/form.md) · [field](/docs/components/field.md) · [password-input](/docs/components/password-input.md) · [app-shell](/docs/components/app-shell.md) · [alert](/docs/components/alert.md)
