Auth Layout
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.
import { AuthContent, AuthDescription, AuthDivider, AuthError, … } from "sebs7n-ui/auth-layout"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.
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`.
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.
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.
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
Generadas del TypeScript del paquete. Las propias del componente, más las heredadas del primitivo que tienen algo que explicar —marcadas «heredada de Base UI»—. El resto está en la línea «hereda de».
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. |
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
- Tab
- Recorre la barra (si hay), los proveedores, el formulario y los links del pie, en ese orden.
- —
- Con
autoFocusenAuthContent, el foco arranca en el primer campo y no en «Continuar con Google».
Accesibilidad
- Un solo `<h1>`:
AuthTitle, o el título deAuthStatuscuando reemplaza al formulario (h1 por defecto;titleAs="h2"si queda unAuthTitlearriba). AuthLayoutpone el<main>(mainIdpara el «Ir al contenido» de la app); labarqueda afuera. Si el layout de la app ya tiene su<main>,as="div".- Errores: los de un campo, en su
FieldErrorcon elerrorsdeForm, que enfoca el primero. El que no es de ningún campo («Email o contraseña incorrectos») va enAuthError, arriba de los campos:role="alert", ícono además del color ytabIndex=-1para poder llevarle el foco. Con unid, el botón de enviar puede nombrarlo enaria-describedby. AuthStatusesrole="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 sonaria-hiddeny el «o» se lee, para que se entienda que lo que sigue es otra forma de entrar. Traducible conlabels.auth.or.autoFocuses 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 conAuthContent variant="plain". - Proveedores arriba del formulario, con
AuthDivideren el medio, yvariant="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
Alertal principio deAuthContent, antes de los proveedores. - Los términos: una casilla obligatoria de verdad (
Checkbox requireden unField), no un botón apagado; al enviar sin tildar,Formlleva el foco ahí. El texto legal pasivo va enAuthFooter, entext-footnote. AuthFooter: «¿No tenés cuenta? Registrate» conlinkVariants({ variant: "inline" }); «Volver a iniciar sesión» o «Cerrar sesión» consubtle. Varios links legales, en un<nav aria-label="Legales">.- Arriba en el teléfono, centrada en alto desde
sm.ambientpinta el wallpaper de iCloud y pasa la card al material translúcido; los campos siguen opacos. - Solo por subpath (
sebs7n-ui/auth-layout).