Polish the public auth experience
Replace the stacked sign-in card with a responsive public shell, a local monochrome compute field, restrained grain and reduced-motion-safe drift. Share the anonymous header with Learn so public navigation stays consistent, and carry the corrected spacing through registration and profile setup.
This commit is contained in:
+144
-149
@@ -12,10 +12,10 @@
|
||||
* mistakes.
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { KeyRound, Mail } from 'lucide-react';
|
||||
import { Mail } from 'lucide-react';
|
||||
import { getSupabase, type PublicConfig } from '@/lib/api';
|
||||
import { Button, Card, CardContent, Input } from '@/components/ui';
|
||||
import { PiggyMark } from '@/components/PiggyMark';
|
||||
import { Button, Input } from '@/components/ui';
|
||||
import { AuthShell } from '@/components/AuthShell';
|
||||
import { usePageTitle } from '@/lib/title';
|
||||
|
||||
type Method = 'password' | 'link';
|
||||
@@ -75,159 +75,154 @@ export function SignIn({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-dvh items-center justify-center bg-bg px-6 py-12">
|
||||
<div className="w-full max-w-sm">
|
||||
<div className="mb-8 flex flex-col items-center gap-3 text-center">
|
||||
<PiggyMark className="h-14 w-14 text-fg" title="pig" />
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold lowercase tracking-tight">pig</h1>
|
||||
<p className="text-sm text-muted">Prime Intellect Growth</p>
|
||||
<AuthShell>
|
||||
<div className="auth-panel rounded-2xl border border-border/80 bg-surface/90 p-5 shadow-2xl backdrop-blur-xl sm:p-7 lg:rounded-none lg:border-0 lg:bg-transparent lg:p-0 lg:shadow-none lg:backdrop-blur-none">
|
||||
{status === 'sent' ? (
|
||||
<div className="flex flex-col items-center gap-4 py-4 text-center sm:py-8">
|
||||
<span className="flex size-12 items-center justify-center rounded-full border border-border bg-surface-2">
|
||||
<Mail className="size-5 text-accent-fg" aria-hidden />
|
||||
</span>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Check your email</h1>
|
||||
<p className="max-w-sm text-sm leading-6 text-muted">
|
||||
A sign-in link is on its way to {email}. It expires shortly, so use it soon.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStatus('idle')}
|
||||
className="tap text-sm font-medium text-accent-fg"
|
||||
>
|
||||
Use a different address
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-5">
|
||||
{status === 'sent' ? (
|
||||
<div className="space-y-3 text-center">
|
||||
<Mail className="mx-auto h-8 w-8 text-accent-fg" aria-hidden />
|
||||
<div>
|
||||
<p className="font-medium">Check your email</p>
|
||||
<p className="mt-1 text-sm text-muted">
|
||||
A sign-in link is on its way to {email}. It expires shortly, so use it
|
||||
soon.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-xs font-medium uppercase tracking-[0.14em] text-muted">
|
||||
Private workspace
|
||||
</p>
|
||||
<h1 className="text-3xl font-medium tracking-[-0.035em] sm:text-4xl">
|
||||
Sign in to PIG
|
||||
</h1>
|
||||
<p className="max-w-md text-sm leading-6 text-muted sm:text-[0.9375rem]">
|
||||
Authentication stays with the deployment's identity provider. PIG never stores your
|
||||
password.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
role="tablist"
|
||||
aria-label="Sign-in method"
|
||||
className="mt-6 flex w-full border-b border-border"
|
||||
>
|
||||
{(
|
||||
[
|
||||
{ key: 'password', label: 'Password' },
|
||||
{ key: 'link', label: 'Email link' },
|
||||
] as const
|
||||
).map((option) => (
|
||||
<button
|
||||
key={option.key}
|
||||
type="button"
|
||||
onClick={() => setStatus('idle')}
|
||||
className="tap text-sm font-medium text-accent-fg"
|
||||
role="tab"
|
||||
aria-selected={method === option.key}
|
||||
onClick={() => {
|
||||
setMethod(option.key);
|
||||
setStatus('idle');
|
||||
setMessage('');
|
||||
}}
|
||||
className={[
|
||||
'tap relative flex flex-1 items-center justify-center px-3 text-sm font-medium transition-colors',
|
||||
method === option.key
|
||||
? 'text-fg after:absolute after:inset-x-3 after:-bottom-px after:h-px after:bg-primary'
|
||||
: 'text-muted hover:text-fg',
|
||||
].join(' ')}
|
||||
>
|
||||
Use a different address
|
||||
{option.label}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-5">
|
||||
<p className="text-xs font-medium uppercase tracking-[0.14em] text-muted">
|
||||
Private workspace
|
||||
))}
|
||||
</div>
|
||||
|
||||
<form onSubmit={submit} className="mt-5 flex flex-col gap-4">
|
||||
<label className="block" htmlFor="sign-in-email">
|
||||
<span className="mb-1 block text-sm font-medium">Email</span>
|
||||
<Input
|
||||
id="sign-in-email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="you@company.com"
|
||||
autoComplete="username"
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{method === 'password' ? (
|
||||
<label className="block" htmlFor="sign-in-password">
|
||||
<span className="mb-1 block text-sm font-medium">Password</span>
|
||||
<Input
|
||||
id="sign-in-password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
// `current-password` is what lets a password manager
|
||||
// offer to fill, and iOS to offer a saved credential.
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
className="mt-1 w-full"
|
||||
disabled={status === 'busy'}
|
||||
>
|
||||
{status === 'busy'
|
||||
? method === 'password'
|
||||
? 'Signing in…'
|
||||
: 'Sending…'
|
||||
: method === 'password'
|
||||
? 'Sign in'
|
||||
: 'Email me a sign-in link'}
|
||||
</Button>
|
||||
|
||||
{status === 'error' ? (
|
||||
<p className="text-sm text-danger" role="alert">
|
||||
{message}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{config.canSelfRegister ? (
|
||||
<div className="mt-1 border-t border-border pt-4">
|
||||
<p className="text-sm leading-6 text-muted">
|
||||
Have a PIG invite code but no account? Registration remains closed on the shared
|
||||
identity provider.
|
||||
</p>
|
||||
<h2 className="mt-1 text-lg font-semibold">Sign in to PIG</h2>
|
||||
<p className="mt-1 text-sm text-muted">
|
||||
Authentication stays with the deployment's identity provider. PIG never stores your password.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
role="tablist"
|
||||
aria-label="Sign-in method"
|
||||
className="mb-4 inline-flex w-full rounded-lg bg-surface-2 p-1"
|
||||
>
|
||||
{(
|
||||
[
|
||||
{ key: 'password', label: 'Password', icon: KeyRound },
|
||||
{ key: 'link', label: 'Email link', icon: Mail },
|
||||
] as const
|
||||
).map((option) => (
|
||||
<button
|
||||
key={option.key}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={method === option.key}
|
||||
onClick={() => {
|
||||
setMethod(option.key);
|
||||
setStatus('idle');
|
||||
setMessage('');
|
||||
}}
|
||||
className={[
|
||||
'tap flex flex-1 items-center justify-center gap-2 rounded-md px-3 text-sm font-medium transition-colors',
|
||||
method === option.key ? 'bg-surface text-fg shadow-sm' : 'text-muted',
|
||||
].join(' ')}
|
||||
>
|
||||
<option.icon className="h-4 w-4" aria-hidden />
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<form onSubmit={submit} className="space-y-3">
|
||||
<label className="block" htmlFor="sign-in-email">
|
||||
<span className="mb-1 block text-sm font-medium">Email</span>
|
||||
<Input
|
||||
id="sign-in-email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="you@company.com"
|
||||
autoComplete="username"
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{method === 'password' ? (
|
||||
<label className="block" htmlFor="sign-in-password">
|
||||
<span className="mb-1 block text-sm font-medium">Password</span>
|
||||
<Input
|
||||
id="sign-in-password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
// `current-password` is what lets a password manager
|
||||
// offer to fill, and iOS to offer a saved credential.
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
disabled={status === 'busy'}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCreateAccount}
|
||||
className="tap -ml-2 inline-flex items-center px-2 text-sm font-medium text-accent-fg"
|
||||
>
|
||||
{status === 'busy'
|
||||
? method === 'password'
|
||||
? 'Signing in…'
|
||||
: 'Sending…'
|
||||
: method === 'password'
|
||||
? 'Sign in'
|
||||
: 'Email me a sign-in link'}
|
||||
</Button>
|
||||
|
||||
{status === 'error' ? (
|
||||
<p className="text-sm text-danger" role="alert">
|
||||
{message}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{config.canSelfRegister ? (
|
||||
<div className="border-t border-border pt-3 text-center">
|
||||
<p className="text-sm text-muted">
|
||||
Have a PIG invite code but no account? Registration remains closed on the shared identity provider.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCreateAccount}
|
||||
className="tap mt-1 text-sm font-medium text-accent-fg"
|
||||
>
|
||||
Create an account
|
||||
</button>
|
||||
</div>
|
||||
) : config.inviteRequired ? (
|
||||
<p className="pt-1 text-center text-xs text-muted">
|
||||
This workspace is invite-only. Ask an administrator to provision access; this screen never opens self-registration.
|
||||
</p>
|
||||
) : null}
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
Create an account
|
||||
</button>
|
||||
</div>
|
||||
) : config.inviteRequired ? (
|
||||
<p className="pt-1 text-center text-xs text-muted">
|
||||
This workspace is invite-only. Ask an administrator to provision access; this
|
||||
screen never opens self-registration.
|
||||
</p>
|
||||
) : null}
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AuthShell>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user