// LoginScreen — KARE Partner App PA-001
// AuthLayout, mobile 375px
const KARE = {
primary: '#0D7A6E',
primaryBgHover: '#E6F4F2',
primaryPressed: '#085C53',
gold: '#C9A84C',
n900: '#111827', n700: '#374151', n500: '#6B7280',
n300: '#D1D5DB', n100: '#F3F4F6', n000: '#FFFFFF',
error: '#DC2626', errorBg: '#FEF2F2',
warning: '#D97706', warningBg: '#FFFBEB',
success: '#059669', successBg: '#ECFDF5',
};
const KO = {
partner: 'Partner',
title: '파트너 로그인',
subtitle: 'Sign in to manage your services',
email: '이메일',
emailPh: 'name@partner.com',
password: '비밀번호',
pwPh: '비밀번호 입력',
show: '표시',
hide: '숨김',
signIn: '로그인',
forgot: '비밀번호 찾기',
invalid: '이메일 또는 비밀번호가 올바르지 않습니다',
accountSuspended: '계정이 정지되었습니다. KARE 지원팀에 문의하세요',
partnerSuspended: '파트너사가 정지 상태입니다. 조회 전용 모드로 진입합니다',
copyright: '© KARE 2026 · Partner App',
};
const EN = {
partner: 'Partner',
title: 'Partner Sign In',
subtitle: 'Sign in to manage your services',
email: 'Email',
emailPh: 'name@partner.com',
password: 'Password',
pwPh: 'Enter password',
show: 'Show',
hide: 'Hide',
signIn: 'Sign in',
forgot: 'Forgot password',
invalid: 'Incorrect email or password',
accountSuspended: 'Your account is suspended. Please contact KARE support.',
partnerSuspended: 'Partner is suspended. Entering view-only mode.',
copyright: '© KARE 2026 · Partner App',
};
const COPY = { ko: KO, en: EN };
// ─────────────────────────────────────────────────────────────
// KARE logo — wordmark + accent square (no complex SVG)
// ─────────────────────────────────────────────────────────────
function KareLogo({ size = 80 }) {
// size = total square footprint; render wordmark + small accent
const fs = Math.round(size * 0.36);
return (
);
}
// ─────────────────────────────────────────────────────────────
// Lang toggle (top-right)
// ─────────────────────────────────────────────────────────────
function LangToggle({ lang, onChange }) {
const opt = (v, label) => (
);
return (
{opt('ko', 'KO')}
{opt('en', 'EN')}
);
}
// ─────────────────────────────────────────────────────────────
// Text input
// ─────────────────────────────────────────────────────────────
function TextField({
label, value, onChange, placeholder, type = 'text',
error = false, trailing, autoComplete, onFocus, onBlur, focused,
}) {
const border = error ? KARE.error : (focused ? KARE.primary : KARE.n300);
return (
);
}
// ─────────────────────────────────────────────────────────────
// Primary button — 56px glove-friendly
// ─────────────────────────────────────────────────────────────
function PrimaryBtn({ children, disabled, loading, onClick }) {
const bg = disabled ? KARE.n300 : KARE.primary;
return (
);
}
function Spinner() {
return (
);
}
// ─────────────────────────────────────────────────────────────
// Toast — top-anchored, error or warning
// ─────────────────────────────────────────────────────────────
function Toast({ kind = 'error', children, top = 56 }) {
const palette = kind === 'warning'
? { fg: KARE.warning, bg: KARE.warningBg, border: '#FBE5BE', icon: '!' }
: { fg: KARE.error, bg: KARE.errorBg, border: '#FBD5D5', icon: '!' };
return (
{palette.icon}
{children}
);
}
// ─────────────────────────────────────────────────────────────
// Inline error
// ─────────────────────────────────────────────────────────────
function InlineError({ children }) {
return (
);
}
// ─────────────────────────────────────────────────────────────
// Eye icon (show/hide password)
// ─────────────────────────────────────────────────────────────
function EyeBtn({ shown, onToggle }) {
return (
);
}
// ─────────────────────────────────────────────────────────────
// LoginScreen — full screen, designed for 375 viewport
// ─────────────────────────────────────────────────────────────
// state: 'default' | 'filled' | 'loading' | 'invalid'
// | 'accountSuspended' | 'partnerSuspended'
// interactive: if true, manages its own state from a 'default' baseline
// ─────────────────────────────────────────────────────────────
function LoginScreen({ state: stateProp = 'default', interactive = false, initialLang = 'ko' }) {
const [lang, setLang] = React.useState(initialLang);
const t = COPY[lang];
// Interactive state
const [email, setEmail] = React.useState('');
const [pw, setPw] = React.useState('');
const [showPw, setShowPw] = React.useState(false);
const [focused, setFocused] = React.useState(null);
const [loading, setLoading] = React.useState(false);
const [invalid, setInvalid] = React.useState(false);
const [toast, setToast] = React.useState(null); // 'accountSuspended' | 'partnerSuspended' | null
// Static state overrides (for static showcase artboards)
let _email = email, _pw = pw, _showPw = showPw, _loading = loading,
_invalid = invalid, _toast = toast;
if (!interactive) {
if (stateProp === 'default') {
_email = ''; _pw = '';
} else if (stateProp === 'filled') {
_email = 'jiwon.kim@seoulmed.kr'; _pw = '••••••••••';
} else if (stateProp === 'loading') {
_email = 'jiwon.kim@seoulmed.kr'; _pw = '••••••••••'; _loading = true;
} else if (stateProp === 'invalid') {
_email = 'jiwon.kim@seoulmed.kr'; _pw = '••••••••••'; _invalid = true;
} else if (stateProp === 'accountSuspended') {
_email = 'jiwon.kim@seoulmed.kr'; _pw = '••••••••••';
_toast = 'accountSuspended';
} else if (stateProp === 'partnerSuspended') {
_email = 'jiwon.kim@seoulmed.kr'; _pw = '••••••••••';
_toast = 'partnerSuspended';
}
}
const canSubmit = _email.trim().length > 0 && _pw.length > 0 && !_loading;
const handleSubmit = () => {
if (!interactive || !canSubmit) return;
setInvalid(false); setToast(null); setLoading(true);
// Demo behavior: short timeout → invalid (unless special pwd)
setTimeout(() => {
setLoading(false);
if (pw === 'kare2026') {
setToast('partnerSuspended'); // success-but-suspended demo
} else if (pw === 'suspended') {
setToast('accountSuspended');
} else {
setInvalid(true);
}
}, 1200);
};
return (
{/* Lang toggle, top-right, below status bar */}
{}} />
{/* Toast layer */}
{_toast === 'accountSuspended' && (
{t.accountSuspended}
)}
{_toast === 'partnerSuspended' && (
{t.partnerSuspended}
)}
{/* Main content — centered */}
{/* Logo + Partner caption */}
{/* Card */}
{}}
placeholder={t.emailPh}
type="email"
autoComplete="email"
error={_invalid}
focused={focused === 'email'}
onFocus={() => setFocused('email')}
onBlur={() => setFocused(null)}
/>
{}}
placeholder={t.pwPh}
type={_showPw ? 'text' : 'password'}
autoComplete="current-password"
error={_invalid}
focused={focused === 'password'}
onFocus={() => setFocused('password')}
onBlur={() => setFocused(null)}
trailing={
setShowPw(!showPw) : () => {}}
/>
}
/>
{_invalid && {t.invalid}}
{t.signIn}
{/* Footer copyright */}
{t.copyright}
);
}
Object.assign(window, {
LoginScreen, KARE, KareLogo, LangToggle,
TextField, PrimaryBtn, Spinner, Toast, InlineError, EyeBtn, COPY,
});