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:
co-authored by
Claude Opus 5
parent
47e33fa311
commit
23398c4ac0
+55
-19
@@ -238,18 +238,34 @@ export function AuthModal({ mode: initialMode, onClose, onAuthed }) {
|
|||||||
const [mode, setMode] = useState(initialMode || 'register')
|
const [mode, setMode] = useState(initialMode || 'register')
|
||||||
const [email, setEmail] = useState('')
|
const [email, setEmail] = useState('')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
|
const [reveal, setReveal] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [busy, setBusy] = useState(false)
|
const [busy, setBusy] = useState(false)
|
||||||
const registering = mode === 'register'
|
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) => {
|
const submit = async (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setBusy(true)
|
setBusy(true)
|
||||||
setError('')
|
setError('')
|
||||||
try {
|
try {
|
||||||
|
const address = email.trim()
|
||||||
const me = registering
|
const me = registering
|
||||||
? await api.register(email, password)
|
? await api.register(address, password)
|
||||||
: await api.login(email, password)
|
: await api.login(address, password)
|
||||||
onAuthed(me, mode)
|
onAuthed(me, mode)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
@@ -259,34 +275,54 @@ export function AuthModal({ mode: initialMode, onClose, onAuthed }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="modal-overlay" onClick={onClose}>
|
<div className="modal-overlay" onClick={onClose}>
|
||||||
<form className="modal" onClick={(e) => e.stopPropagation()} onSubmit={submit}>
|
<form className="modal auth-modal" onClick={(e) => e.stopPropagation()} onSubmit={submit}
|
||||||
<h2>{registering ? 'Create an account' : 'Log in'}</h2>
|
aria-labelledby="auth-title">
|
||||||
<p className="modal-hint">
|
<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
|
{registering
|
||||||
? 'Everything you’ve played as a guest stays with your new account, and you can pick it up from any device.'
|
? '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>
|
</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">
|
<label className="field">
|
||||||
<span className="label">Email</span>
|
<span className="label">Email</span>
|
||||||
<input type="email" autoFocus required value={email}
|
<input type="email" autoFocus required value={email}
|
||||||
|
autoComplete="email" placeholder="you@example.com"
|
||||||
onChange={(e) => setEmail(e.target.value)} />
|
onChange={(e) => setEmail(e.target.value)} />
|
||||||
</label>
|
</label>
|
||||||
<label className="field">
|
<label className="field">
|
||||||
<span className="label">Password{registering ? ' (at least 8 characters)' : ''}</span>
|
<span className="label">Password</span>
|
||||||
<input type="password" required minLength={registering ? 8 : undefined} value={password}
|
<div className="auth-password">
|
||||||
onChange={(e) => setPassword(e.target.value)} />
|
<input type={reveal ? 'text' : 'password'} required
|
||||||
</label>
|
minLength={registering ? 8 : undefined} value={password}
|
||||||
{error && <div className="test-error" style={{ marginTop: 4 }}>{error}</div>}
|
autoComplete={registering ? 'new-password' : 'current-password'}
|
||||||
<div className="modal-buttons" style={{ justifyContent: 'space-between', alignItems: 'center' }}>
|
onChange={(e) => setPassword(e.target.value)} />
|
||||||
<button type="button" className="linklike" onClick={() => { setMode(registering ? 'login' : 'register'); setError('') }}>
|
<button type="button" className="auth-reveal" tabIndex={-1}
|
||||||
{registering ? 'Have an account? Log in' : 'New here? Create an account'}
|
aria-label={reveal ? 'Hide password' : 'Show password'}
|
||||||
</button>
|
onClick={() => setReveal((r) => !r)}>
|
||||||
<div style={{ display: 'flex', gap: 10 }}>
|
{reveal ? 'Hide' : 'Show'}
|
||||||
<button type="button" onClick={onClose}>Cancel</button>
|
|
||||||
<button type="submit" className="primary" disabled={busy}>
|
|
||||||
{busy ? '…' : registering ? 'Sign up' : 'Log in'}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+72
-2
@@ -184,7 +184,7 @@ label.field .label {
|
|||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
margin-bottom: 5px;
|
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%;
|
width: 100%;
|
||||||
background: var(--bg-input);
|
background: var(--bg-input);
|
||||||
color: var(--text);
|
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-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; }
|
.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. */
|
/* "Update from scenario" — a wider modal, since it lists a diff. */
|
||||||
.refresh-modal { width: min(620px, calc(100vw - 40px)); }
|
.refresh-modal { width: min(620px, calc(100vw - 40px)); }
|
||||||
.refresh-changes {
|
.refresh-changes {
|
||||||
@@ -1984,7 +2054,7 @@ button.primary.compact { padding: 3px 12px; font-size: 0.76rem; margin-left: aut
|
|||||||
/* ---------- Global page padding / typography ---------- */
|
/* ---------- Global page padding / typography ---------- */
|
||||||
.page { padding: 18px 14px; }
|
.page { padding: 18px 14px; }
|
||||||
/* 16px inputs stop iOS Safari auto-zooming when a field is focused. */
|
/* 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;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user