Auth Password Reset
blockauthFour-stage password reset surface in a single card: request (email form) -> sent (check-your-inbox confirmation with "Open reset form" demo button) -> reset (new password + confirm with match validation) -> done (success confirmation linking back to sign-in). Emits `request` (email) when the link is asked for and `reset` (password) when the new password is set; consumer wires the actual mail/db calls.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/lit/auth-password-reset.json$npx shadcn@latest add https://uipkge.dev/r/lit/auth-password-reset.json$yarn dlx shadcn@latest add https://uipkge.dev/r/lit/auth-password-reset.json$bunx shadcn@latest add https://uipkge.dev/r/lit/auth-password-reset.jsonManifest:
https://uipkge.dev/r/lit/auth-password-reset.jsonInstalls to:src/uip/blocks/auth-password-reset/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
sign-in-href | string | /login | optional |
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
HTMLElementTagNameMapinterface HTMLElementTagNameMap {
'uip-auth-password-reset': UipAuthPasswordReset
}
}Files installed (1)
src/uip/blocks/auth-password-reset/auth-password-reset.ts8.6 kB
import { LitElement, css, html } from 'lit' import { ArrowLeft, MailCheck } from 'lucide' import { cn } from '../../lib/utils' import { tailwind } from '../../lib/styles' import { ThemeController } from '../../lib/theme' import { icon } from '../../lib/icon' // Side-effect imports: register the composed elements for standalone use // (consumers who install only this block still get working primitives). import '../../components/button/button' import '../../components/card/card' import '../../components/input/input' import '../../components/label/label' import type { UipInput } from '../../components/input/input' type Stage = 'request' | 'sent' | 'reset' | 'done' /** * <uip-auth-password-reset> — the registry AuthPasswordReset block as a web component. * * Class strings are React's AuthPasswordReset verbatim. React state becomes * @state fields. `onRequest` (email) / `onReset` (password) become `request` * / `reset` CustomEvents (bubbles, composed). The sent-stage "Open reset * form" demo button and the stage machine are unchanged. */ export class UipAuthPasswordReset extends LitElement { static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true } // :host display can't be a utility class (the host has no template of its own). static styles = [tailwind, css`:host { display: block; }`] static properties = { signInHref: { attribute: 'sign-in-href' }, stage: { state: true }, email: { state: true }, password: { state: true }, confirm: { state: true }, } signInHref = '/login' private stage: Stage = 'request' private email = '' private password = '' private confirm = '' constructor() { super() new ThemeController(this) } connectedCallback() { super.connectedCallback() this.setAttribute('data-uipkge', '') this.setAttribute('data-slot', 'auth-password-reset') } private submitRequest(e: Event) { e.preventDefault() if (!this.email) return this.dispatchEvent(new CustomEvent('request', { detail: this.email, bubbles: true, composed: true })) this.stage = 'sent' } private submitReset(e: Event) { e.preventDefault() const match = !this.confirm || this.password === this.confirm if (!this.password || !match) return this.dispatchEvent(new CustomEvent('reset', { detail: this.password, bubbles: true, composed: true })) this.stage = 'done' } render() { const passwordsMatch = !this.confirm || this.password === this.confirm return html` <div data-slot="auth-password-reset" class=${cn('bg-background flex min-h-svh items-center justify-center p-6')}> <uip-card class="w-full max-w-sm"> ${this.stage === 'request' ? html` <uip-card-header class="text-center"> <uip-card-title class="text-2xl">Forgot password?</uip-card-title> <uip-card-description>Enter your email and we'll send you a reset link.</uip-card-description> </uip-card-header> <uip-card-content> <form class="space-y-4" @submit=${this.submitRequest}> <div class="grid gap-2"> <uip-label for="reset-email">Email</uip-label> <uip-input id="reset-email" .value=${this.email} type="email" placeholder="you@company.com" required @input=${(e: Event) => { this.email = (e.target as UipInput).value }} ></uip-input> </div> <uip-button type="submit" class="w-full">Send reset link</uip-button> </form> </uip-card-content> <uip-card-footer class="justify-center"> <a href=${this.signInHref} class="text-muted-foreground hover:text-foreground inline-flex items-center gap-1 text-sm" > ${icon(ArrowLeft, 'arrow-left', 'size-3')} Back to sign in </a> </uip-card-footer> ` : ''} ${this.stage === 'sent' ? html` <uip-card-content class="space-y-4 pt-6 text-center"> <div class="bg-primary/10 text-primary mx-auto flex size-12 items-center justify-center rounded-full"> ${icon(MailCheck, 'mail-check', 'size-6')} </div> <div class="space-y-1"> <h3 class="text-lg font-semibold">Check your inbox</h3> <p class="text-muted-foreground text-sm"> We've sent a reset link to <span class="text-foreground font-medium">${this.email}</span>. </p> </div> <uip-button variant="outline" class="w-full" @click=${() => { this.stage = 'reset' }} >Open reset form (demo)</uip-button > <button class="text-muted-foreground hover:text-foreground text-xs underline-offset-4 hover:underline" @click=${() => { this.stage = 'request' }} > Wrong email? </button> </uip-card-content> ` : ''} ${this.stage === 'reset' ? html` <uip-card-header class="text-center"> <uip-card-title class="text-2xl">Set new password</uip-card-title> <uip-card-description>Pick a strong password you haven't used before.</uip-card-description> </uip-card-header> <uip-card-content> <form class="space-y-4" @submit=${this.submitReset}> <div class="grid gap-2"> <uip-label for="reset-pw">New password</uip-label> <uip-input id="reset-pw" .value=${this.password} type="password" autocomplete="new-password" required @input=${(e: Event) => { this.password = (e.target as UipInput).value }} ></uip-input> </div> <div class="grid gap-2"> <uip-label for="reset-confirm">Confirm password</uip-label> <uip-input id="reset-confirm" .value=${this.confirm} type="password" autocomplete="new-password" aria-invalid=${passwordsMatch ? 'false' : 'true'} required @input=${(e: Event) => { this.confirm = (e.target as UipInput).value }} ></uip-input> ${!passwordsMatch ? html`<p class="text-destructive text-xs">Passwords don't match.</p>` : ''} </div> <uip-button type="submit" class="w-full">Reset password</uip-button> </form> </uip-card-content> ` : ''} ${this.stage === 'done' ? html` <uip-card-content class="space-y-4 pt-6 text-center"> <div class="bg-success/10 text-success mx-auto flex size-12 items-center justify-center rounded-full"> ${icon(MailCheck, 'mail-check', 'size-6')} </div> <div class="space-y-1"> <h3 class="text-lg font-semibold">All set</h3> <p class="text-muted-foreground text-sm"> Your password has been updated. You can now sign in with the new password. </p> </div> <a href=${this.signInHref}><uip-button class="w-full">Continue to sign in</uip-button></a> </uip-card-content> ` : ''} </uip-card> </div> ` } } customElements.get('uip-auth-password-reset') || customElements.define('uip-auth-password-reset', UipAuthPasswordReset) declare global { interface HTMLElementTagNameMap { 'uip-auth-password-reset': UipAuthPasswordReset } }
Raw manifest:https://uipkge.dev/r/lit/auth-password-reset.json