Fix the mismatched email field and rework the auth modal

The global input styling in index.css enumerates types, and `email` was
never in the list, so the Email box fell through to browser defaults while
the Password box right below it got the app treatment. Added it to both
copies of that selector list -- the base rule and the 16px iOS-zoom rule in
the mobile block; missing the second would revert the fix on phones.

While in there, the modal itself: log in / sign up are now segmented tabs
instead of a link crammed into the button row, fields carry autoComplete so
password managers work at all, a show/hide toggle on the password, Esc to
dismiss, and the error is a real role=alert box rather than the shared
.test-error text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UeQVy5bEjLhfgWNc27Efet
This commit is contained in:
parththakkar106
2026-08-06 20:47:04 +05:30
co-authored by Claude Opus 5
parent 47e33fa311
commit 23398c4ac0
2 changed files with 127 additions and 21 deletions
+55 -19
View File
@@ -238,18 +238,34 @@ export function AuthModal({ mode: initialMode, onClose, onAuthed }) {
const [mode, setMode] = useState(initialMode || 'register')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [reveal, setReveal] = useState(false)
const [error, setError] = useState('')
const [busy, setBusy] = useState(false)
const registering = mode === 'register'
// Esc dismisses, like the overlay click already does.
useEffect(() => {
const onKey = (e) => { if (e.key === 'Escape') onClose() }
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [onClose])
const switchTo = (next) => {
if (next === mode) return
setMode(next)
setError('')
setReveal(false)
}
const submit = async (e) => {
e.preventDefault()
setBusy(true)
setError('')
try {
const address = email.trim()
const me = registering
? await api.register(email, password)
: await api.login(email, password)
? await api.register(address, password)
: await api.login(address, password)
onAuthed(me, mode)
} catch (err) {
setError(err.message)
@@ -259,34 +275,54 @@ export function AuthModal({ mode: initialMode, onClose, onAuthed }) {
return (
<div className="modal-overlay" onClick={onClose}>
<form className="modal" onClick={(e) => e.stopPropagation()} onSubmit={submit}>
<h2>{registering ? 'Create an account' : 'Log in'}</h2>
<p className="modal-hint">
<form className="modal auth-modal" onClick={(e) => e.stopPropagation()} onSubmit={submit}
aria-labelledby="auth-title">
<div className="auth-crest" aria-hidden="true">❖</div>
<h2 id="auth-title">{registering ? 'Create an account' : 'Welcome back'}</h2>
<p className="modal-hint auth-hint">
{registering
? 'Everything you’ve played as a guest stays with your new account, and you can pick it up from any device.'
: 'Welcome back — log in to reach your adventures.'}
: 'Log in to reach your adventures.'}
</p>
<div className="auth-tabs" role="tablist">
<button type="button" role="tab" aria-selected={!registering}
className={`auth-tab${registering ? '' : ' active'}`}
onClick={() => switchTo('login')}>Log in</button>
<button type="button" role="tab" aria-selected={registering}
className={`auth-tab${registering ? ' active' : ''}`}
onClick={() => switchTo('register')}>Sign up</button>
</div>
<label className="field">
<span className="label">Email</span>
<input type="email" autoFocus required value={email}
autoComplete="email" placeholder="you@example.com"
onChange={(e) => setEmail(e.target.value)} />
</label>
<label className="field">
<span className="label">Password{registering ? ' (at least 8 characters)' : ''}</span>
<input type="password" required minLength={registering ? 8 : undefined} value={password}
onChange={(e) => setPassword(e.target.value)} />
</label>
{error && <div className="test-error" style={{ marginTop: 4 }}>{error}</div>}
<div className="modal-buttons" style={{ justifyContent: 'space-between', alignItems: 'center' }}>
<button type="button" className="linklike" onClick={() => { setMode(registering ? 'login' : 'register'); setError('') }}>
{registering ? 'Have an account? Log in' : 'New here? Create an account'}
</button>
<div style={{ display: 'flex', gap: 10 }}>
<button type="button" onClick={onClose}>Cancel</button>
<button type="submit" className="primary" disabled={busy}>
{busy ? '…' : registering ? 'Sign up' : 'Log in'}
<span className="label">Password</span>
<div className="auth-password">
<input type={reveal ? 'text' : 'password'} required
minLength={registering ? 8 : undefined} value={password}
autoComplete={registering ? 'new-password' : 'current-password'}
onChange={(e) => setPassword(e.target.value)} />
<button type="button" className="auth-reveal" tabIndex={-1}
aria-label={reveal ? 'Hide password' : 'Show password'}
onClick={() => setReveal((r) => !r)}>
{reveal ? 'Hide' : 'Show'}
</button>
</div>
{registering && <span className="auth-help">At least 8 characters.</span>}
</label>
{error && <div className="auth-error" role="alert">{error}</div>}
<div className="modal-buttons">
<button type="button" onClick={onClose}>Cancel</button>
<button type="submit" className="primary" disabled={busy}>
{busy ? 'Please wait…' : registering ? 'Sign up' : 'Log in'}
</button>
</div>
</form>
</div>
+72 -2
View File
@@ -184,7 +184,7 @@ label.field .label {
color: var(--text-dim);
margin-bottom: 5px;
}
input[type="text"], input[type="password"], input[type="number"], textarea, select {
input[type="text"], input[type="email"], input[type="password"], input[type="number"], textarea, select {
width: 100%;
background: var(--bg-input);
color: var(--text);
@@ -1262,6 +1262,76 @@ button:disabled { opacity: 0.45; cursor: default; transform: none; box-shadow: n
.modal .modal-hint { margin: 0 0 18px; color: var(--text-dim); font-size: 0.88rem; }
.modal .modal-buttons { display: flex; justify-content: flex-end; gap: 10px; margin-top: 18px; }
/* ---------- Auth (log in / sign up) ---------- */
.auth-modal { width: min(400px, calc(100vw - 40px)); text-align: center; padding-top: 22px; }
.auth-crest {
font-size: 1.1rem;
color: var(--accent-dim);
letter-spacing: 0.3em;
margin-bottom: 6px;
}
.auth-modal h2 { font-size: 1.35rem; }
.auth-modal .auth-hint { margin-bottom: 16px; line-height: 1.45; }
/* Fields go back to left-aligned — only the header is centred. */
.auth-modal label.field { text-align: left; }
.auth-tabs {
display: flex;
gap: 4px;
padding: 4px;
margin-bottom: 18px;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 10px;
}
.auth-tab {
flex: 1;
padding: 7px 0;
border: 1px solid transparent;
border-radius: 7px;
background: transparent;
color: var(--text-dim);
font-family: var(--font-display);
font-size: 0.78rem;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.auth-tab:hover { color: var(--text); background: rgba(212, 169, 78, 0.06); }
.auth-tab.active {
background: var(--bg-panel);
border-color: var(--accent-dim);
color: var(--accent-bright);
}
.auth-password { display: flex; gap: 8px; }
.auth-password input { flex: 1; min-width: 0; }
.auth-reveal {
flex: none;
padding: 0 12px;
font-size: 0.78rem;
color: var(--text-dim);
}
.auth-reveal:hover { color: var(--accent-bright); }
.auth-help {
display: block;
margin-top: 6px;
font-size: 0.76rem;
color: var(--text-dim);
}
.auth-error {
margin-top: 4px;
padding: 9px 12px;
border: 1px solid rgba(208, 101, 101, 0.45);
border-radius: 8px;
background: rgba(208, 101, 101, 0.09);
color: var(--danger);
font-size: 0.85rem;
text-align: left;
white-space: pre-wrap;
}
/* "Update from scenario" — a wider modal, since it lists a diff. */
.refresh-modal { width: min(620px, calc(100vw - 40px)); }
.refresh-changes {
@@ -1984,7 +2054,7 @@ button.primary.compact { padding: 3px 12px; font-size: 0.76rem; margin-left: aut
/* ---------- Global page padding / typography ---------- */
.page { padding: 18px 14px; }
/* 16px inputs stop iOS Safari auto-zooming when a field is focused. */
input[type="text"], input[type="password"], input[type="number"], textarea, select {
input[type="text"], input[type="email"], input[type="password"], input[type="number"], textarea, select {
font-size: 16px;
}