afe36fd347
Claude Design export for SD-0001 (login + create-storefront surfaces): wf-*/hf-* screens, design-system bundle, offline preview. First artifact in the ui/designs/ collection (ecomm#8). Also ignore .DS_Store. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
117 lines
6.5 KiB
React
117 lines
6.5 KiB
React
// hf-signin.jsx — Sign in (PUC-2 / PUC-3) hi-fi. Email → one-time code + states.
|
|
const { HFScreen: SScreen, HFWordmark: SWordmark, HFTopBar: STopBar, HFFooter: SFooter,
|
|
HFAuthCard: SAuthCard, HFField: SField, HFCodeField: SCodeField, HFBanner: SBanner,
|
|
HFPrimary: SPrimary, HFHeading: SHeading, HFStateCard: SStateCard, HFStates: SStates,
|
|
HF_H: S, hfHonest: sHonest } = window;
|
|
|
|
function SBack({ label = 'Back' }) {
|
|
return <a className="lk" href="#" style={{ fontFamily: S.body, fontSize: 14, color: S.mute }}>← {label}</a>;
|
|
}
|
|
|
|
// ── Step 1 · email ──────────────────────────────────────────────────────────
|
|
function EmailStep({ framing = 'signup', s = {} }) {
|
|
const login = framing === 'login';
|
|
return (
|
|
<>
|
|
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: 2 }}><SWordmark size={24} /></div>
|
|
<SHeading sub={login ? 'Enter your email and we\u2019ll send a one-time code to sign in.' : 'Enter your email to begin. There\u2019s no password to create.'}>
|
|
{login ? 'Log in' : 'Create your storefront'}
|
|
</SHeading>
|
|
{s.deliveryFail && <SBanner title="We couldn't send the code">The email didn't go out. Try again in a moment — nothing was lost.</SBanner>}
|
|
<SField label="Email" placeholder="you@example.com" value={s.email}
|
|
focus={!s.emailError && !s.email} error={s.emailError} />
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
<SPrimary disabled={!!s.cooldown}>{s.cooldown ? `Resend in ${s.cooldown}s` : 'Send code'}</SPrimary>
|
|
<p style={sHonest}>We'll email you a one-time code. That's all we need — no password, ever.</p>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
// ── Step 2 · code ────────────────────────────────────────────────────────────
|
|
function CodeStep({ s = {} }) {
|
|
const email = s.email || 'ben@bensbets.com';
|
|
let err = null;
|
|
if (s.codeWrong) err = `That code didn't match — ${s.attempts ?? 2} attempts left.`;
|
|
if (s.codeExpired) err = 'That code expired. Send a fresh one below.';
|
|
return (
|
|
<>
|
|
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: 2 }}><SWordmark size={24} /></div>
|
|
<SHeading sub={<>We sent a 6-digit code to <span style={{ color: S.star }}>{email}</span>. <a className="lk" href="#">Wrong address?</a></>}>
|
|
Check your email
|
|
</SHeading>
|
|
{s.welcome && <SBanner tone="info" title={s.welcome === 'new' ? 'Welcome to ecomm' : 'Welcome back'}>
|
|
{s.welcome === 'new' ? 'A new account was created for this email.' : 'Signed in to your existing account.'}
|
|
</SBanner>}
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, alignItems: 'center' }}>
|
|
<SCodeField filled={s.filled ?? 3} error={!!(s.codeWrong || s.codeExpired)} />
|
|
{err && <span style={{ fontFamily: S.body, fontSize: 13, color: S.gold, display: 'flex', gap: 6, alignSelf: 'flex-start' }}><span aria-hidden="true" style={{ fontSize: 11 }}>◆</span>{err}</span>}
|
|
</div>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
<SPrimary>Continue</SPrimary>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
<p style={sHonest}>Good for 10 minutes.</p>
|
|
<a className="lk" href="#" style={{ fontFamily: S.disp, fontWeight: 500, fontSize: 13.5, color: s.cooldown ? S.mute : S.lilac }}>
|
|
{s.cooldown ? `Resend in ${s.cooldown}s` : 'Resend code'}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function HFSignIn({ step = 'email', framing = 'signup', device = 'desktop', s = {} }) {
|
|
const desktop = device === 'desktop';
|
|
return (
|
|
<SScreen horizon>
|
|
<STopBar pad={desktop ? '0 36px' : '0 22px'} left={<SWordmark size={desktop ? 22 : 20} />} right={<SBack />} />
|
|
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: desktop ? 32 : 18 }}>
|
|
<SAuthCard width={desktop ? 432 : '100%'}>
|
|
{step === 'email' ? <EmailStep framing={framing} s={s} /> : <CodeStep s={s} />}
|
|
</SAuthCard>
|
|
</div>
|
|
{desktop && <SFooter />}
|
|
</SScreen>
|
|
);
|
|
}
|
|
|
|
// ── States stack ─────────────────────────────────────────────────────────────
|
|
function codeNote(text) {
|
|
return <span style={{ fontFamily: S.body, fontSize: 13, color: S.gold, display: 'flex', gap: 6, marginTop: 10 }}><span aria-hidden="true" style={{ fontSize: 11 }}>◆</span>{text}</span>;
|
|
}
|
|
function HFSignInStates() {
|
|
return (
|
|
<SStates>
|
|
<SStateCard label="Invalid email" note="inline field error">
|
|
<SField label="Email" value="ben@bensbets" error="That doesn't look like an email address." />
|
|
</SStateCard>
|
|
<SStateCard label="Resend cooldown" note="PUC-2c · 60s per email">
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 11 }}>
|
|
<SPrimary disabled>Resend in 47s</SPrimary>
|
|
<span style={sHonest}>One code per minute keeps inboxes — and abuse — in check.</span>
|
|
</div>
|
|
</SStateCard>
|
|
<SStateCard label="Delivery failed" note="502 · honest failure (INV-9)">
|
|
<SBanner title="We couldn't send the code">The email didn't go out — try again. We'd never fake a success.</SBanner>
|
|
</SStateCard>
|
|
<SStateCard label="Wrong code" note="PUC-2a">
|
|
<SCodeField filled={6} error />{codeNote("That code didn't match — 2 attempts left.")}
|
|
</SStateCard>
|
|
<SStateCard label="Expired code" note="PUC-2b · 10-min window">
|
|
<SCodeField filled={6} error />{codeNote('That code expired. Send a fresh one below.')}
|
|
</SStateCard>
|
|
<SStateCard label="Attempts exhausted" note="5 tries · code invalidated (INV-3)">
|
|
<SBanner title="Too many attempts">That code is no longer valid. Request a fresh one to keep going.</SBanner>
|
|
</SStateCard>
|
|
<SStateCard label="Honest welcome" note="PUC-3 · same flow, true label">
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
<SBanner tone="info" title="Welcome to ecomm">A new account was created for this email.</SBanner>
|
|
<SBanner tone="info" title="Welcome back">Signed in to your existing account.</SBanner>
|
|
</div>
|
|
</SStateCard>
|
|
</SStates>
|
|
);
|
|
}
|
|
|
|
Object.assign(window, { HFSignIn, HFSignInStates });
|