Polish the public auth experience
CI / verify (push) Successful in 3m26s
CI / publish (push) Has been skipped

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:
2026-08-13 20:51:34 -07:00
parent b63ed0181f
commit 9db53cb36f
9 changed files with 574 additions and 430 deletions
+144 -149
View File
@@ -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>
);
}