// Login / signup / invite-redeem screen. // Non-technical UX: one big email field, one password field, smart mode swap. const loginStyles = { page: { minHeight:'100vh', display:'flex', alignItems:'center', justifyContent:'center', background:'var(--paper-2)', fontFamily:'var(--sans)', padding:'40px 16px', }, card: { width:420, background:'var(--paper)', borderRadius:16, boxShadow:'0 24px 60px rgba(0,0,0,.08)', padding:'40px 36px 32px 36px', }, brand: { display:'flex', alignItems:'center', gap:10, marginBottom:24 }, brandImg: { width:36, height:36 }, title: { fontFamily:'var(--serif)', fontSize:28, color:'var(--ink)', letterSpacing:'-.01em', lineHeight:1.1, marginBottom:4 }, sub: { fontSize:13, color:'var(--ink-3)', marginBottom:22, fontFamily:'var(--serif)', fontStyle:'italic' }, tabs: { display:'flex', gap:6, marginBottom:16 }, tab: (a) => ({ padding:'6px 12px', borderRadius:999, border:'1px solid ' + (a ? 'var(--ink)' : 'var(--line-2)'), background: a ? 'var(--ink)' : 'var(--paper)', color: a ? 'var(--paper)' : 'var(--ink-2)', fontSize:11.5, fontFamily:'var(--mono)', letterSpacing:'.06em', textTransform:'uppercase', cursor:'pointer', }), label: { fontFamily:'var(--mono)', fontSize:10, letterSpacing:'.14em', color:'var(--ink-3)', textTransform:'uppercase', marginBottom:6 }, input: { width:'100%', border:'1px solid var(--line-2)', borderRadius:8, padding:'10px 12px', fontSize:14, background:'var(--paper-2)', outline:'none', marginBottom:14, fontFamily:'var(--sans)', color:'var(--ink)', }, primary: { width:'100%', height:44, border:'1px solid var(--ink)', background:'var(--ink)', color:'var(--paper)', borderRadius:999, fontSize:13.5, cursor:'pointer', fontFamily:'var(--sans)', marginTop:8, }, soft: { width:'100%', height:38, border:'1px solid var(--line-2)', background:'var(--paper)', color:'var(--ink-2)', borderRadius:999, fontSize:12.5, cursor:'pointer', fontFamily:'var(--sans)', marginTop:10, }, err: { fontSize:12, color:'var(--accent)', marginBottom:6 }, ok: { fontSize:12, color:'var(--accent-2)', marginBottom:6 }, small: { fontSize:11, color:'var(--ink-4)', textAlign:'center', marginTop:18, fontFamily:'var(--sans)' }, inviteTag: { fontFamily:'var(--mono)', fontSize:10, letterSpacing:'.1em', padding:'3px 8px', borderRadius:999, background:'var(--hl)', color:'var(--accent)', textTransform:'uppercase', marginLeft:8, }, }; function LoginScreen({ inviteToken, onReady }) { const [mode, setMode] = React.useState(inviteToken ? 'signup' : 'magic'); // 'magic' | 'password' | 'signup' const [email, setEmail] = React.useState(''); const [password, setPassword] = React.useState(''); const [busy, setBusy] = React.useState(false); const [err, setErr] = React.useState(null); const [note, setNote] = React.useState(null); const [invite, setInvite] = React.useState(null); React.useEffect(() => { if (!inviteToken) return; API.checkInvite(inviteToken) .then(setInvite) .catch(e => setErr('초대장 오류 — ' + e.message)); }, [inviteToken]); React.useEffect(() => { if (invite && invite.email) setEmail(invite.email); }, [invite]); const onMagic = async () => { if (!email.trim()) { setErr('이메일을 입력해주세요'); return; } setBusy(true); setErr(null); setNote(null); try { const { error } = await window.palette_auth.signInMagic(email.trim()); if (error) { setErr(error.message || '전송 실패'); return; } setNote('이메일로 로그인 링크를 보냈어요. 받은편지함을 확인해주세요.'); } finally { setBusy(false); } }; const onPassword = async () => { if (!email.trim() || !password) { setErr('이메일과 비밀번호를 모두 입력해주세요'); return; } setBusy(true); setErr(null); try { const { error } = await window.palette_auth.signInPassword(email.trim(), password); if (error) { setErr(error.message || '로그인 실패'); return; } onReady && onReady(); } finally { setBusy(false); } }; const onSignup = async () => { if (!email.trim() || !password || password.length < 8) { setErr('이메일과 8자 이상 비밀번호가 필요해요'); return; } setBusy(true); setErr(null); setNote(null); try { const res = await window.palette_auth.signUpWithInvite({ email: email.trim(), password, invite_token: inviteToken, }); if (res.error) { setErr(res.error.message || '가입 실패'); return; } setNote('가입 완료 — 확인 메일을 보냈어요. 메일의 링크를 눌러 로그인하세요.'); } finally { setBusy(false); } }; return (
Palette { e.currentTarget.style.display='none'; }}/>
Palette
Keyword Agent
{mode === 'signup' ? '팀에 합류' : '돌아오신 걸 환영해요'} {invite && 초대 · {invite.role}}
{mode === 'signup' ? 'Join your team — 초대 링크로 접속했어요. 이메일과 비밀번호를 입력하세요.' : '이메일로 빠르게 로그인하세요. Sign in with your work email.'}
{!inviteToken && (
)} {err &&
⚠ {err}
} {note &&
✓ {note}
}
이메일 · Email
setEmail(e.target.value)} placeholder="you@palette.co" onKeyDown={e => { if (e.key === 'Enter' && mode === 'magic') onMagic(); }}/> {mode !== 'magic' && ( <>
비밀번호 · Password {mode==='signup' && · 최소 8자}
setPassword(e.target.value)} placeholder="••••••••" onKeyDown={e => { if (e.key === 'Enter') { mode==='signup' ? onSignup() : onPassword(); } }}/> )} {!inviteToken && mode !== 'signup' && ( )}
Palette Corp · 팀 초대장으로만 가입 가능합니다.
Ask your CD for an invite link. Magic-link emails arrive from supabase.
); } window.LoginScreen = LoginScreen;