# LoginForm

> Presentational sign-in form with email and password, magic link, remember me, provider buttons, passkey and a dev-only shortcut, built-in en and ar strings, linked errors and focus on the first invalid field.

Source: https://docs.nasaqui.com/components/login-form

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/login-form.json
```

A sign-in form that owns no auth logic. It validates the two fields, calls your `onSubmit(values)`, and shows what
you return: nothing for success, or `{ error, fieldErrors }` for a failure. Provider, passkey, magic-link and dev
buttons are optional and hidden unless you supply their handlers. `methods` picks password, magic link or both; an
empty list shows "Sign-in is not available" instead of the form.

## When to use

- The sign-in screen, inside `AuthLayout`.

## When not to use

- Second factor: use `TwoFactorChallenge`.
- Creating an account: use `RegisterForm`.
- Different methods per address (SSO domains, passwordless accounts): use `SignInFlow`, which asks for the email first.

## Import

```tsx
import { LoginForm } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import { LoginForm } from "@fadymondy/nasaq/web";

export function SignIn() {
  return (
    <LoginForm
      forgotPassword={<a href="/forgot-password">Forgot password?</a>}
      onSubmit={async ({ email, password }) => {
        const res = await api.signIn(email, password);
        if (!res.ok) return { error: "Incorrect email or password." };
      }}
    />
  );
}

declare const api: { signIn(email: string, password: string): Promise<{ ok: boolean }> };
```

## Anatomy

```
LoginForm                        data-slot="login-form" (form, noValidate), data-state="idle"
├─ error summary (when needed)   role="alert"
├─ Field: email                  autocomplete="username" (or "username webauthn", or "email" for magic link only)
├─ Field: password               autocomplete="current-password", forgotPassword slot beside the label
├─ remember me checkbox
├─ submit Button                 loading while onSubmit runs ("Email me a sign-in link" when magic link only)
├─ magic-link Button             data-slot="login-form-magic-link" (password and magic link both on)
├─ passkey button + OAuthButtons (optional, after an "or" divider)
└─ dev Button                    data-slot="login-form-dev", dashed (onDevLogin only)

data-state="sent"                after a link is sent: icon, heading (focused), body, resend (data-slot="login-form-resend"), change email
data-state="blocked"             methods={[]}: a warning Alert, and the dev button when set
```

## API

**LoginForm**: every `form` prop except `onSubmit` and `children`, plus:

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `onSubmit` | `(values: LoginValues) => Promise<AuthSubmitResult> \| AuthSubmitResult` | required | `LoginValues` is `{ email, password, remember }`. Return `{ error?, fieldErrors? }` to fail. A throw shows `labels.failed`. |
| `defaultEmail` | `string` | | Prefill the email. |
| `forgotPassword` | `ReactNode` | | Slot beside the password label, usually a link. |
| `showRemember` | `boolean` | `true` | Show "Remember me". |
| `oauthProviders` | `OAuthProvider[]` | | Show provider buttons. |
| `onOAuth` | `(id: string) => void \| Promise<unknown>` | | Provider click handler. |
| `onPasskey` | `() => void \| Promise<unknown>` | | Shows "Sign in with a passkey" when the browser has WebAuthn. |
| `onPasskeyAutofill` | `(signal: AbortSignal) => void \| Promise<unknown>` | | Conditional UI: started on mount when supported, aborted on unmount. |
| `onMagicLink` | `({ email }) => Promise<AuthSubmitResult> \| AuthSubmitResult` | | Sends a one-time link. Adds "Email me a sign-in link", which checks the email only, then shows "Check your email" with a resend timer. |
| `magicLinkSeconds` | `number` | `30` | Seconds before the link can be sent again. |
| `methods` | `readonly LoginMethod[]` | password, plus `"magic-link"` when `onMagicLink` is set | `LoginMethod` is `"password" \| "magic-link"`. `["magic-link"]` drops the password field. `[]` shows the blocked notice. |
| `onDevLogin` | `() => Promise<AuthSubmitResult> \| AuthSubmitResult` | | A dashed "Dev login" button for local builds. `{ error }` or a throw (`labels.devFailed`) shows in the summary. Never pass it in production. |
| `labels` | `Partial<LoginFormLabels>` | English or Arabic | Every string, including validation messages. |

`fieldErrors` keys: `email`, `password`.

## Examples

**Field error from the server**

```tsx
import { LoginForm } from "@fadymondy/nasaq/web";

export function Locked() {
  return <LoginForm onSubmit={async () => ({ fieldErrors: { email: "No account uses this email." } })} />;
}
```

**Password or magic link**

```tsx
import { LoginForm } from "@fadymondy/nasaq/web";

export function SignIn() {
  return (
    <LoginForm
      onSubmit={async ({ email, password }) => api.signIn(email, password)}
      onMagicLink={async ({ email }) => api.sendLink(email)}
    />
  );
}

declare const api: { signIn(email: string, password: string): Promise<void>; sendLink(email: string): Promise<void> };
```

**Magic link only, with a dev shortcut in local builds**

```tsx
import { LoginForm } from "@fadymondy/nasaq/web";

export function Passwordless() {
  return (
    <LoginForm
      methods={["magic-link"]}
      onSubmit={() => {}}
      onMagicLink={async ({ email }) => api.sendLink(email)}
      onDevLogin={import.meta.env.DEV ? () => api.devLogin() : undefined}
    />
  );
}

declare const api: { sendLink(email: string): Promise<void>; devLogin(): Promise<void> };
```

**Passkeys and providers**

```tsx
import { LoginForm } from "@fadymondy/nasaq/web";

export function Full() {
  return (
    <LoginForm
      oauthProviders={["google", "github"]}
      onOAuth={(id) => redirectTo(id)}
      onPasskey={() => signInWithPasskey()}
      onPasskeyAutofill={(signal) => signInWithPasskey(signal)}
      onSubmit={async () => {}}
    />
  );
}

declare function redirectTo(id: string): Promise<void>;
declare function signInWithPasskey(signal?: AbortSignal): Promise<void>;
```

## Accessibility

- The form is `noValidate` and validates itself, so messages are yours to localise and read in one place. Every field
  has a visible `<label>`; an invalid field gets `aria-invalid` and its message is linked with `aria-describedby`.
- On a failed submit focus moves to the first invalid field. When the failure is for the whole form (`error`) the
  summary at the top of the form (`role="alert"`) is focused instead, so it is announced.
- The submit button shows a spinner and is `aria-busy` while `onSubmit` runs; a second submit is ignored.
- Autocomplete: `username` on email (`username webauthn` when passkey autofill is on) and `current-password` on the
  password, so password managers and passkeys work.
- The passkey button is not rendered when `PublicKeyCredential` is missing, so nobody meets a dead control.
- After a magic link is sent the "Check your email" heading takes focus inside a `role="status"` region, so the change
  is announced. The resend button counts down and a polite status says when a new link went out.
- The blocked notice is an `Alert` with a title, so it reads as one message.

## RTL & i18n

- Built-in English and Arabic strings follow the Nasaq locale (`useNasaq`). Pass `labels` to change any of them or to
  add another language; keys you omit keep the built-in text.
- Layout uses logical properties, so it mirrors under `dir="rtl"` with no extra work.
- The email is entered left to right (`dir="ltr"` on the input value) even in Arabic, as addresses are Latin.

## Styling & tokens

- Built from `Field`, `Input`, `PasswordInput`, `Checkbox`, `Button` and `Alert`; all colours are `--nq-*` tokens.
- Extend the form with `className`; it is a vertical flex stack.

## Do / Don't

- Do return `{ error }` for a wrong password rather than throwing.
- Do keep the error generic so it does not reveal which accounts exist. Say "Check your email" after a magic link
  even when no account uses the address.
- Do gate `onDevLogin` behind a development flag.
- Don't validate credentials on the client.
- Don't hide the forgot-password link.

## Related

- [`auth-layout`](https://docs.nasaqui.com/components/auth-layout)
- [`sign-in-flow`](https://docs.nasaqui.com/components/sign-in-flow)
- [`oauth-buttons`](https://docs.nasaqui.com/components/oauth-buttons)
- [`register-form`](https://docs.nasaqui.com/components/register-form)
- [`forgot-password-form`](https://docs.nasaqui.com/components/forgot-password-form)
- [`two-factor-challenge`](https://docs.nasaqui.com/components/two-factor-challenge)

## Lab

https://docs.nasaqui.com/?path=/docs/components-auth-login-form--docs

## Code

### React

```tsx
import { LoginForm } from "@fadymondy/nasaq/web";

export function SignIn() {
  return (
    <LoginForm
      forgotPassword={<a href="/forgot-password">Forgot password?</a>}
      onSubmit={async ({ email, password }) => {
        const res = await api.signIn(email, password);
        if (!res.ok) return { error: "Incorrect email or password." };
      }}
    />
  );
}

declare const api: { signIn(email: string, password: string): Promise<{ ok: boolean }> };
```

### shadcn

```tsx
import { LoginForm } from "@/components/ui/login-form";

export function SignIn() {
  return (
    <LoginForm
      forgotPassword={<a href="/forgot-password">Forgot password?</a>}
      onSubmit={async ({ email, password }) => {
        const res = await api.signIn(email, password);
        if (!res.ok) return { error: "Incorrect email or password." };
      }}
    />
  );
}

declare const api: { signIn(email: string, password: string): Promise<{ ok: boolean }> };
```

### Vue

```vue
<script setup lang="ts">
import { NqLoginForm, type LoginValues } from "@fadymondy/nasaq/vue";

// Your API call. Return { error } for a wrong password.
async function signIn({ email, password }: LoginValues) {
  await new Promise((r) => setTimeout(r, 400));
  if (password !== "correct-horse") return { error: "Incorrect email or password." };
}
</script>

<template>
  <div class="w-80">
    <NqLoginForm :on-submit="signIn">
      <template #forgotPassword><a href="/forgot-password" class="underline underline-offset-2">Forgot password?</a></template>
    </NqLoginForm>
  </div>
</template>
```

### Blade

```blade
{{-- Your sign-in call. Resolve { error: "…" } for a wrong password; the magic link is optional. --}}
<div class="w-80">
    <x-nq::login-form :methods="['password', 'magic-link']" :oauth-providers="['google', 'github']" :passkey="true"
        x-on:nq-login="$event.detail.waitUntil(new Promise((done) => setTimeout(() => done($event.detail.password === 'correct-horse' ? undefined : { error: 'Incorrect email or password.' }), 300)))"
        x-on:nq-magic-link="$event.detail.waitUntil(new Promise((done) => setTimeout(done, 300)))">
        <x-slot:forgot-password><a href="/forgot-password" class="underline underline-offset-2">Forgot password?</a></x-slot:forgot-password>
    </x-nq::login-form>
</div>
```

### HTML + Alpine

```html
<div class="w-80">
    <form data-slot="login-form" novalidate x-data="nqLoginForm(JSON.parse('{\u0022password\u0022:true,\u0022magicLink\u0022:true,\u0022magicLinkSeconds\u0022:30,\u0022oauth\u0022:true,\u0022passkey\u0022:true,\u0022passkeyAutofill\u0022:false,\u0022names\u0022:[\u0022email\u0022,\u0022password\u0022],\u0022fieldLabels\u0022:{\u0022email\u0022:\u0022Email\u0022,\u0022password\u0022:\u0022Password\u0022},\u0022errorTitle\u0022:\u0022Fix these to sign in\u0022,\u0022failed\u0022:\u0022Something went wrong. Try again.\u0022,\u0022labels\u0022:{\u0022email\u0022:\u0022Email\u0022,\u0022emailPlaceholder\u0022:\u0022you@example.com\u0022,\u0022password\u0022:\u0022Password\u0022,\u0022remember\u0022:\u0022Remember me\u0022,\u0022submit\u0022:\u0022Sign in\u0022,\u0022passkey\u0022:\u0022Sign in with a passkey\u0022,\u0022divider\u0022:\u0022or\u0022,\u0022emailRequired\u0022:\u0022Enter your email address.\u0022,\u0022emailInvalid\u0022:\u0022Enter a valid email address.\u0022,\u0022passwordRequired\u0022:\u0022Enter your password.\u0022,\u0022errorTitle\u0022:\u0022Fix these to sign in\u0022,\u0022failed\u0022:\u0022Something went wrong. Try again.\u0022,\u0022magicLink\u0022:\u0022Email me a sign-in link\u0022,\u0022sentTitle\u0022:\u0022Check your email\u0022,\u0022sentBody\u0022:\u0022We sent a sign-in link to {email}. It works once and expires soon.\u0022,\u0022resend\u0022:\u0022Send the link again\u0022,\u0022resendIn\u0022:\u0022Send again in {time}\u0022,\u0022resent\u0022:\u0022We sent a new link.\u0022,\u0022changeEmail\u0022:\u0022Use a different email\u0022,\u0022devLogin\u0022:\u0022Dev login\u0022,\u0022devFailed\u0022:\u0022Dev login failed.\u0022,\u0022blockedTitle\u0022:\u0022Sign-in is not available\u0022,\u0022blockedBody\u0022:\u0022This account can\\u0027t sign in here. Contact your administrator.\u0022}}'))" x-bind:data-state="sentTo ? 'sent' : 'idle'"
        x-bind:aria-busy="pending ? 'true' : null" x-on:submit.prevent="onSubmit()" x-on:input="clear($event.target.name)"
        x-on:nq-login="$event.detail.waitUntil(new Promise((done) =&gt; setTimeout(() =&gt; done($event.detail.password === &#039;correct-horse&#039; ? undefined : { error: &#039;Incorrect email or password.&#039; }), 300)))" x-on:nq-magic-link="$event.detail.waitUntil(new Promise((done) =&gt; setTimeout(done, 300)))" class="flex w-full flex-col gap-4">
        <div class="contents" x-show="! sentTo">
            <div tabindex="-1" class="sr-only" x-show="! problems()"></div>
<div tabindex="-1" data-slot="auth-error-summary" class="outline-none" x-show="problems()" style="display: none">
    <div data-slot="alert" data-tone="danger" role="alert" class="relative grid grid-cols-[auto_1fr_auto] items-start gap-x-3 rounded-card border p-3 text-start border-nq-danger/30 bg-nq-danger-soft">
        <svg aria-hidden="true" data-slot="alert-icon" class="mt-0.5 size-4 text-nq-danger-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
  <path d="m15 9-6 6"/>
  <path d="m9 9 6 6"/>
</svg>        <div data-slot="alert-body" class="flex min-w-0 flex-col gap-0.5">
            <div data-slot="alert-title" class="text-label text-foreground" x-text="summaryTitle()"></div>
            <div data-slot="alert-description" class="text-body-sm text-muted-foreground" x-show="! error && entries().length" style="display: none">
                <ul class="flex list-disc flex-col gap-0.5 ps-4">
                    <template x-for="e in entries()" :key="e.name">
                        <li><button type="button" class="text-start underline underline-offset-2" x-on:click="focusField(e.name)" x-text="(e.label ? e.label + ': ' : '') + e.message"></button></li>
                    </template>
                </ul>
            </div>
        </div>
    </div>
</div>
            <div data-slot="field" x-data="nqField(false)" x-modelable="invalid" x-id="['nq-field']"
         data-valid     x-model="bad.email" class="flex flex-col gap-1.5">
    <label data-slot="field-label"  for="nq-login-000001-email" class="text-label text-foreground data-disabled:opacity-50">Email</label>
                <input data-slot="input" type="email"
     dir="ltr"      name="email"             id="nq-login-000001-email" inputmode="email" value="" autocomplete="username" placeholder="you@example.com" aria-describedby="nq-login-000001-email-error" x-bind:aria-invalid="bad.email ? &#039;true&#039; : null" class="w-full min-w-0 rounded-control border border-input bg-card px-3 text-body text-foreground min-h-[var(--nq-touch-min,0px)] transition-colors duration-150 ease-nq outline-none placeholder:text-muted-foreground focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-invalid:border-nq-danger aria-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px] h-control text-start">
                <div data-slot="field-error" role="alert" x-show="invalid"  style="display: none"  id="nq-login-000001-email-error" class="text-caption text-nq-danger-text"><span x-text="fe.email"></span></div>
</div>
                            <div data-slot="field" x-data="nqField(false)" x-modelable="invalid" x-id="['nq-field']"
         data-valid     x-model="bad.password" class="flex flex-col gap-1.5">
    <div class="flex items-baseline justify-between gap-3">
                        <label data-slot="field-label"  for="nq-login-000001-password" class="text-label text-foreground data-disabled:opacity-50">Password</label>
                                                    <span class="text-caption"><a href="/forgot-password" class="underline underline-offset-2">Forgot password?</a></span>
                                            </div>
                    <div data-slot="password-input" x-data="nqPasswordInput('', JSON.parse('{\u0022visible\u0022:false,\u0022score\u0022:null,\u0022rules\u0022:null,\u0022levels\u0022:[\u0022Very weak\u0022,\u0022Weak\u0022,\u0022Fair\u0022,\u0022Good\u0022,\u0022Strong\u0022]}'))" x-modelable="value"
    class="flex w-full flex-col gap-2">
    <div role="group" data-slot="input-group"
    class="group/input-group flex h-control min-h-[var(--nq-touch-min,0px)] w-full min-w-0 items-center overflow-hidden rounded-control border border-input bg-card text-body text-foreground transition-colors duration-150 ease-nq focus-within:border-nq-focus focus-within:outline-1 focus-within:outline-nq-focus has-[[data-invalid]]:border-nq-danger has-[[aria-invalid=true]]:border-nq-danger has-[input:disabled]:cursor-not-allowed has-[input:disabled]:opacity-50"><input data-slot="input-group-input"     x-model="value" x-bind:type="shown ? &#039;text&#039; : &#039;password&#039;" type="password" value="" autocapitalize="none" autocorrect="off" spellcheck="false" id="nq-login-000001-password" name="password" autocomplete="current-password" aria-describedby="nq-login-000001-password-error" x-bind:aria-invalid="bad.password ? &#039;true&#039; : null" class="h-full min-w-0 flex-1 border-0 bg-transparent px-3 text-body text-foreground outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed pointer-coarse:text-[16px]" />
        <div data-slot="input-group-addon" data-align="end"
    class="flex h-full shrink-0 items-center gap-1.5 text-body-sm text-muted-foreground [&_svg]:size-4 order-last ps-1 pe-1.5"><button data-slot="password-input-toggle"
     type="button"                         x-bind="toggle" aria-label="Show password" aria-pressed="false" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground hover:bg-nq-hover size-control-sm p-0">
        <svg x-show="shown" x-cloak="1" style="display: none" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/>
  <path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/>
  <path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/>
  <path d="m2 2 20 20"/>
</svg>                <svg x-show="!shown" x-cloak="1" style="" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/>
  <circle cx="12" cy="12" r="3"/>
</svg></button></div></div>
        </div>
                    <div data-slot="field-error" role="alert" x-show="invalid"  style="display: none"  id="nq-login-000001-password-error" class="text-caption text-nq-danger-text"><span x-text="fe.password"></span></div>
</div>
                                    <label class="flex items-center gap-2 text-body-sm text-foreground">
                        <button type="button" role="checkbox" data-slot="checkbox" x-data="nqCheckbox(false, false)" x-modelable="checked" x-bind="root"
    aria-checked="false"
     data-unchecked                 x-model="remember" class="relative inline-flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-nq-line-strong bg-card text-primary-foreground outline-none transition-colors duration-150 ease-nq data-checked:border-primary data-checked:bg-primary data-indeterminate:border-primary data-indeterminate:bg-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus data-disabled:cursor-not-allowed data-disabled:opacity-50 after:absolute after:-inset-1">
    <span data-slot="checkbox-indicator" class="flex items-center justify-center [&_svg]:size-3 [&_svg]:stroke-3" x-show="checked || indeterminate"  style="display: none" >
        <svg aria-hidden="true" x-show="indeterminate" style="display: none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M5 12h14"/>
</svg>        <svg aria-hidden="true" x-show="! indeterminate" style="" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg>    </span>
    <input type="hidden" name="remember" value="1" x-bind:disabled="! checked"  disabled ></button>
                        Remember me
                    </label>
                                        <button data-slot="button"
     type="submit"                         x-bind:disabled="offFor(&#039;password&#039;)" x-bind:data-disabled="offFor(&#039;password&#039;) ? &#039;&#039; : null" x-bind:aria-busy="loadingFor(&#039;password&#039;) ? &#039;true&#039; : null" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--nq-action)_88%,var(--nq-fg))] h-[calc(var(--nq-control)+8px)] px-5 text-body">
        <template x-if="loadingFor('password')"><svg data-slot="spinner" aria-hidden="true" class="size-4 animate-spin motion-reduce:animate-none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</svg></template>
                Sign in</button>
                            <button data-slot="login-form-magic-link"
     type="button"                         x-on:click="send(&#039;magic-link&#039;)" x-bind:disabled="offFor(&#039;magic-link&#039;)" x-bind:data-disabled="offFor(&#039;magic-link&#039;) ? &#039;&#039; : null" x-bind:aria-busy="loadingFor(&#039;magic-link&#039;) ? &#039;true&#039; : null" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover h-[calc(var(--nq-control)+8px)] px-5 text-body">
        <template x-if="loadingFor('magic-link')"><svg data-slot="spinner" aria-hidden="true" class="size-4 animate-spin motion-reduce:animate-none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</svg></template>
                    <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M22 13V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h8"/>
  <path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>
  <path d="m16 19 2 2 4-4"/>
</svg>                    Email me a sign-in link</button>
                                        <div data-slot="oauth-divider" role="separator" x-show="dividerOn()" style="display: none" class="flex items-center gap-3 text-caption text-muted-foreground">
    <span aria-hidden="true" class="h-px flex-1 bg-border"></span>
    <span>or</span>
    <span aria-hidden="true" class="h-px flex-1 bg-border"></span>
</div>
                                        <button data-slot="login-form-passkey"
     type="button"                         x-show="passkeyOn" style="display: none" x-on:click="passkey()" x-bind:disabled="passkeyPending || pending || devPending" x-bind:data-disabled="passkeyPending ? &#039;&#039; : null" x-bind:aria-busy="passkeyPending ? &#039;true&#039; : null" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover h-[calc(var(--nq-control)+8px)] px-5 text-body">
        <template x-if="passkeyPending"><svg data-slot="spinner" aria-hidden="true" class="size-4 animate-spin motion-reduce:animate-none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</svg></template>
                    <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z"/>
  <circle cx="16.5" cy="7.5" r=".5" fill="currentColor"/>
</svg>                    Sign in with a passkey</button>
                                        <div role="group" aria-label="Sign in with a provider" data-slot="oauth-buttons" data-layout="stack"
    x-data="nqOAuthButtons(null, false)" x-modelable="pending"
    class="flex w-full flex-col gap-2">
                    <button data-slot="oauth-button"
     type="button"                         data-provider="google" x-on:click="select(&#039;google&#039;)" x-bind:disabled="isDisabled(&#039;google&#039;)" x-bind:aria-busy="isBusy(&#039;google&#039;) ? &#039;true&#039; : null" x-bind:data-disabled="(disabled || isBusy(&#039;google&#039;)) ? &#039;&#039; : null" class="inline-flex shrink-0 select-none items-center whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover h-control px-[var(--nq-control-pad)] relative w-full min-w-0 justify-center gap-3 [&_svg]:size-auto">
        <span class="contents" x-show="! isBusy('google')" >
                                    <svg viewBox="10 10 20 20" width="20" height="20" aria-hidden="true" focusable="false" >
    <path d="M29.6 20.2273C29.6 19.5182 29.5364 18.8364 29.4182 18.1818H20V22.05H25.3818C25.15 23.3 24.4455 24.3591 23.3864 25.0682V27.5773H26.6182C28.5091 25.8364 29.6 23.2727 29.6 20.2273Z" fill="#4285F4" />
    <path d="M20 30C22.7 30 24.9636 29.1045 26.6181 27.5773L23.3863 25.0682C22.4909 25.6682 21.3454 26.0227 20 26.0227C17.3954 26.0227 15.1909 24.2636 14.4045 21.9H11.0636V24.4909C12.7091 27.7591 16.0909 30 20 30Z" fill="#34A853" />
    <path d="M14.4045 21.9C14.2045 21.3 14.0909 20.6591 14.0909 20C14.0909 19.3409 14.2045 18.7 14.4045 18.1V15.5091H11.0636C10.3864 16.8591 10 18.3864 10 20C10 21.6136 10.3864 23.1409 11.0636 24.4909L14.4045 21.9Z" fill="#FBBC04" />
    <path d="M20 13.9773C21.4681 13.9773 22.7863 14.4818 23.8227 15.4727L26.6909 12.6045C24.9591 10.9909 22.6954 10 20 10C16.0909 10 12.7091 12.2409 11.0636 15.5091L14.4045 18.1C15.1909 15.7364 17.3954 13.9773 20 13.9773Z" fill="#E94235" />
</svg>
                            </span>
            <span class="contents" x-show="isBusy('google')"  style="display: none" >
                <svg data-slot="spinner" aria-hidden="true" class="animate-spin motion-reduce:animate-none size-4!" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</svg>            </span>
                                                <span class="truncate">Sign in with <bdi>Google</bdi></span></button>
                    <button data-slot="oauth-button"
     type="button"                         data-provider="github" x-on:click="select(&#039;github&#039;)" x-bind:disabled="isDisabled(&#039;github&#039;)" x-bind:aria-busy="isBusy(&#039;github&#039;) ? &#039;true&#039; : null" x-bind:data-disabled="(disabled || isBusy(&#039;github&#039;)) ? &#039;&#039; : null" class="inline-flex shrink-0 select-none items-center whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover h-control px-[var(--nq-control-pad)] relative w-full min-w-0 justify-center gap-3 [&_svg]:size-auto">
        <span class="contents" x-show="! isBusy('github')" >
                                    <svg viewBox="0 0 98 96" width="20" height="20" aria-hidden="true" focusable="false" >
    <path d="M41.4395 69.3848C28.8066 67.8535 19.9062 58.7617 19.9062 46.9902C19.9062 42.2051 21.6289 37.0371 24.5 33.5918C23.2559 30.4336 23.4473 23.7344 24.8828 20.959C28.7109 20.4805 33.8789 22.4902 36.9414 25.2656C40.5781 24.1172 44.4062 23.543 49.0957 23.543C53.7852 23.543 57.6133 24.1172 61.0586 25.1699C64.0254 22.4902 69.2891 20.4805 73.1172 20.959C74.457 23.543 74.6484 30.2422 73.4043 33.4961C76.4668 37.1328 78.0937 42.0137 78.0937 46.9902C78.0937 58.7617 69.1934 67.6621 56.3691 69.2891C59.623 71.3945 61.8242 75.9883 61.8242 81.252L61.8242 91.2051C61.8242 94.0762 64.2168 95.7031 67.0879 94.5547C84.4102 87.9512 98 70.6289 98 49.1914C98 22.1074 75.9883 6.69539e-07 48.9043 4.309e-07C21.8203 1.92261e-07 -1.9479e-07 22.1074 -4.3343e-07 49.1914C-6.20631e-07 70.4375 13.4941 88.0469 31.6777 94.6504C34.2617 95.6074 36.75 93.8848 36.75 91.3008L36.75 83.6445C35.4102 84.2188 33.6875 84.6016 32.1562 84.6016C25.8398 84.6016 22.1074 81.1563 19.4277 74.7441C18.375 72.1602 17.2266 70.6289 15.0254 70.3418C13.877 70.2461 13.4941 69.7676 13.4941 69.1934C13.4941 68.0449 15.4082 67.1836 17.3223 67.1836C20.0977 67.1836 22.4902 68.9063 24.9785 72.4473C26.8926 75.2227 28.9023 76.4668 31.2949 76.4668C33.6875 76.4668 35.2187 75.6055 37.4199 73.4043C39.0469 71.7773 40.291 70.3418 41.4395 69.3848Z"  class="fill-black dark:fill-white"  />
</svg>
                            </span>
            <span class="contents" x-show="isBusy('github')"  style="display: none" >
                <svg data-slot="spinner" aria-hidden="true" class="animate-spin motion-reduce:animate-none size-4!" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</svg>            </span>
                                                <span class="truncate">Sign in with <bdi>GitHub</bdi></span></button>
    </div>
                                </div>
        <div class="contents" x-show="sentTo" style="display: none">
            <div class="flex size-10 items-center justify-center rounded-full bg-nq-success-soft text-nq-success-text">
                <svg aria-hidden="true" class="size-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M22 13V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h8"/>
  <path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>
  <path d="m16 19 2 2 4-4"/>
</svg>            </div>
            <div role="status" class="flex flex-col gap-1.5">
                <h2 data-slot="login-form-sent-title" tabindex="-1" class="text-h3 text-foreground outline-none">Check your email</h2>
                <p class="text-body-sm text-muted-foreground">We sent a sign-in link to <bdi dir="ltr" class="font-medium text-foreground" x-text="sentTo"></bdi>. It works once and expires soon.</p>
            </div>
            <div data-slot="alert" data-tone="danger" role="alert"
        x-show="resendError" style="display: none" class="relative grid grid-cols-[auto_1fr_auto] items-start gap-x-3 rounded-card border p-3 text-start border-nq-danger/30 bg-nq-danger-soft">
    <svg aria-hidden="true" data-slot="alert-icon" class="mt-0.5 size-4 text-nq-danger-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
  <path d="m15 9-6 6"/>
  <path d="m9 9 6 6"/>
</svg>    <div data-slot="alert-body" class="flex min-w-0 flex-col gap-0.5">
                            <div data-slot="alert-description" class="text-body-sm text-foreground"><span x-text="resendError"></span></div>
            </div>
    </div>
            <span role="status" class="sr-only" x-text="resendDone && cooldown > 0 ? 'We sent a new link.' : ''"></span>
            <button data-slot="login-form-resend"
     type="button"                         x-on:click="resend()" x-bind:disabled="resendOff()" x-bind:data-disabled="resendOff() ? &#039;&#039; : null" x-bind:aria-busy="resendPending ? &#039;true&#039; : null" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover h-[calc(var(--nq-control)+8px)] px-5 text-body">
        <template x-if="resendPending"><svg data-slot="spinner" aria-hidden="true" class="size-4 animate-spin motion-reduce:animate-none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</svg></template>
                <span x-text="resendLabel()">Send the link again</span></button>
            <button data-slot="button"
     type="button"                         x-on:click="changeEmail()" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground underline decoration-nq-line underline-offset-4 hover:decoration-current h-auto px-0 self-start">
        Use a different email</button>
        </div>
    </form>
</div>
```
