// SC AccountPro — Login Screen
// Glass-morphism card, animated AI background, focus glow, logo shimmer, dark mode.
const { useState, useEffect, useRef, useMemo } = React;
// ─────────────────────────────────────────────────────────────
// Theme palettes — keyed by accent + mode
// ─────────────────────────────────────────────────────────────
const PALETTES = {
navy: {
light: {
bgFrom: '#0a2358', bgVia: '#0f3179', bgTo: '#1a4fa6',
cardBg: 'rgba(255,255,255,0.92)', cardBorder: 'rgba(255,255,255,0.6)',
text: '#0b1f4a', textMuted: '#4b5b7a', textSubtle: '#8693ad',
inputBg: '#ffffff', inputBorder: '#e3e8f0', inputBorderFocus: '#2563eb',
btnFrom: '#1d4ed8', btnTo: '#3b82f6', btnGlow: 'rgba(37,99,235,0.45)',
net: 'rgba(120,170,255,0.35)', netGlow: '#5b9eff',
gold: '#f4c14a', goldDeep: '#d99820',
featTint: 'rgba(255,255,255,0.08)', featIcon: '#60a5fa', featText: 'rgba(255,255,255,0.78)',
pillBg: 'rgba(10,25,60,0.55)', pillBorder: 'rgba(244,193,74,0.35)', pillText: '#ffd97a',
footerText: 'rgba(255,255,255,0.55)', footerSub: 'rgba(255,255,255,0.4)'
},
dark: {
bgFrom: '#02060f', bgVia: '#050d22', bgTo: '#0a1a3a',
cardBg: 'rgba(18,28,52,0.78)', cardBorder: 'rgba(120,160,230,0.18)',
text: '#eaf0ff', textMuted: '#94a3c4', textSubtle: '#5d6c8c',
inputBg: 'rgba(8,15,32,0.6)', inputBorder: 'rgba(120,160,230,0.18)', inputBorderFocus: '#60a5fa',
btnFrom: '#1e40af', btnTo: '#3b82f6', btnGlow: 'rgba(96,165,250,0.45)',
net: 'rgba(120,170,255,0.28)', netGlow: '#4f8eff',
gold: '#f4c14a', goldDeep: '#d99820',
featTint: 'rgba(255,255,255,0.04)', featIcon: '#7eb6ff', featText: 'rgba(220,232,255,0.7)',
pillBg: 'rgba(8,18,42,0.7)', pillBorder: 'rgba(244,193,74,0.3)', pillText: '#ffd97a',
footerText: 'rgba(220,232,255,0.45)', footerSub: 'rgba(220,232,255,0.32)'
}
},
emerald: {
light: {
bgFrom: '#052e2a', bgVia: '#0b4a3f', bgTo: '#11705c',
cardBg: 'rgba(255,255,255,0.92)', cardBorder: 'rgba(255,255,255,0.6)',
text: '#04261f', textMuted: '#3a5a52', textSubtle: '#7f9a92',
inputBg: '#ffffff', inputBorder: '#dfeae5', inputBorderFocus: '#0d9488',
btnFrom: '#0f766e', btnTo: '#14b8a6', btnGlow: 'rgba(20,184,166,0.45)',
net: 'rgba(110,220,200,0.32)', netGlow: '#34d4bd',
gold: '#f5cf6a', goldDeep: '#dba83a',
featTint: 'rgba(255,255,255,0.08)', featIcon: '#5eead4', featText: 'rgba(255,255,255,0.78)',
pillBg: 'rgba(4,30,28,0.55)', pillBorder: 'rgba(245,207,106,0.35)', pillText: '#ffe49a',
footerText: 'rgba(255,255,255,0.55)', footerSub: 'rgba(255,255,255,0.4)'
},
dark: {
bgFrom: '#020f0e', bgVia: '#03201d', bgTo: '#053934',
cardBg: 'rgba(14,32,30,0.78)', cardBorder: 'rgba(94,234,212,0.16)',
text: '#e8fbf6', textMuted: '#8fbcb1', textSubtle: '#5a7e76',
inputBg: 'rgba(4,18,16,0.6)', inputBorder: 'rgba(94,234,212,0.16)', inputBorderFocus: '#5eead4',
btnFrom: '#0f766e', btnTo: '#2dd4bf', btnGlow: 'rgba(45,212,191,0.45)',
net: 'rgba(110,220,200,0.28)', netGlow: '#34d4bd',
gold: '#f5cf6a', goldDeep: '#dba83a',
featTint: 'rgba(255,255,255,0.04)', featIcon: '#7eead4', featText: 'rgba(220,250,243,0.7)',
pillBg: 'rgba(4,18,16,0.7)', pillBorder: 'rgba(245,207,106,0.3)', pillText: '#ffe49a',
footerText: 'rgba(220,250,243,0.45)', footerSub: 'rgba(220,250,243,0.32)'
}
},
royal: {
light: {
bgFrom: '#1a0a47', bgVia: '#2d1372', bgTo: '#4f2bae',
cardBg: 'rgba(255,255,255,0.92)', cardBorder: 'rgba(255,255,255,0.6)',
text: '#1a0a47', textMuted: '#574a78', textSubtle: '#8e87a8',
inputBg: '#ffffff', inputBorder: '#e6e3f0', inputBorderFocus: '#7c3aed',
btnFrom: '#6d28d9', btnTo: '#8b5cf6', btnGlow: 'rgba(139,92,246,0.45)',
net: 'rgba(170,140,255,0.32)', netGlow: '#9d7bff',
gold: '#f4c14a', goldDeep: '#d99820',
featTint: 'rgba(255,255,255,0.08)', featIcon: '#c4b5fd', featText: 'rgba(255,255,255,0.78)',
pillBg: 'rgba(20,10,55,0.55)', pillBorder: 'rgba(244,193,74,0.35)', pillText: '#ffd97a',
footerText: 'rgba(255,255,255,0.55)', footerSub: 'rgba(255,255,255,0.4)'
},
dark: {
bgFrom: '#0a0418', bgVia: '#150a35', bgTo: '#241354',
cardBg: 'rgba(28,18,55,0.78)', cardBorder: 'rgba(196,181,253,0.16)',
text: '#f3eeff', textMuted: '#b4a8d5', textSubtle: '#766b94',
inputBg: 'rgba(12,6,30,0.6)', inputBorder: 'rgba(196,181,253,0.16)', inputBorderFocus: '#c4b5fd',
btnFrom: '#5b21b6', btnTo: '#8b5cf6', btnGlow: 'rgba(139,92,246,0.45)',
net: 'rgba(170,140,255,0.28)', netGlow: '#9d7bff',
gold: '#f4c14a', goldDeep: '#d99820',
featTint: 'rgba(255,255,255,0.04)', featIcon: '#c4b5fd', featText: 'rgba(243,238,255,0.7)',
pillBg: 'rgba(12,6,30,0.7)', pillBorder: 'rgba(244,193,74,0.3)', pillText: '#ffd97a',
footerText: 'rgba(243,238,255,0.45)', footerSub: 'rgba(243,238,255,0.32)'
}
}
};
// ─────────────────────────────────────────────────────────────
// Animated network background — buildings + nodes + flowing lines
// ─────────────────────────────────────────────────────────────
function NetworkBg({ p, speed = 1, paused = false, transparent = false }) {
// Deterministic nodes
const nodes = useMemo(() => {
const seed = (s) => {let h = 2166136261;for (let i = 0; i < s.length; i++) h = Math.imul(h ^ s.charCodeAt(i), 16777619);return (h >>> 0) / 4294967295;};
return Array.from({ length: 22 }, (_, i) => ({
x: seed('x' + i) * 100,
y: seed('y' + i) * 60 + (seed('yo' + i) > 0.5 ? 0 : 35),
r: seed('r' + i) * 1.4 + 1.2,
delay: seed('d' + i) * 6
}));
}, []);
// Connections (pairs of nodes within distance)
const links = useMemo(() => {
const out = [];
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
const dx = nodes[i].x - nodes[j].x,dy = nodes[i].y - nodes[j].y;
const d = Math.hypot(dx, dy);
if (d > 8 && d < 24) out.push({ a: i, b: j, d, delay: (i * 7 + j * 13) % 12 });
}
}
return out.slice(0, 36);
}, [nodes]);
const dur = `${(14 / Math.max(0.2, speed)).toFixed(1)}s`;
return (
{/* gradient base — skipped when transparent so the page bg shows through */}
{!transparent && (
)}
{/* soft top glow */}
{/* bottom horizon glow */}
{/* Network SVG */}
{/* Building silhouettes — left */}
{/* mirrored right side — graph chart */}
{/* gold diagonal accents */}
);
}
// ─────────────────────────────────────────────────────────────
// SC AccountPro — fully native SVG brand mark
// Geometric SC monogram + neural circuit mesh + animated nodes,
// ACCOUNTPRO wordmark, gold accent rule, AI badge.
// No PNG — everything is animatable SVG/CSS.
// ─────────────────────────────────────────────────────────────
function SCLogo({ p, paused = false, font = 'Bodoni Moda' }) {
const NAVY = '#0e1f44';
const NAVY_MID = '#1d3766';
const NAVY_LT = '#3a5e9e';
const GOLD = p.gold || '#e8a834';
const GOLD_DK = p.goldDeep || '#b87d12';
const CYAN = '#7eb6ff';
const BLUE = '#3b82f6';
// Per-font size/weight/letter-spacing tuning so different families sit nicely
const fontTuning = {
'Bodoni Moda': { size: 105, weight: 900, ls: -5 },
'Playfair Display': { size: 110, weight: 900, ls: -5 },
'Cinzel': { size: 95, weight: 900, ls: -3 },
'Cormorant Garamond': { size: 118, weight: 700, ls: -7 },
'DM Serif Display': { size: 110, weight: 400, ls: -5 },
'Abril Fatface': { size: 105, weight: 400, ls: -5 },
'Yeseva One': { size: 108, weight: 400, ls: -4 },
'Libre Bodoni': { size: 108, weight: 900, ls: -5 },
'EB Garamond': { size: 118, weight: 700, ls: -7 },
'Cardo': { size: 118, weight: 700, ls: -7 },
'Spectral SC': { size: 92, weight: 800, ls: 0 },
'Marcellus': { size: 108, weight: 400, ls: -3 },
'Italiana': { size: 130, weight: 400, ls: -7 },
'Inter': { size: 100, weight: 800, ls: -7 },
'Bebas Neue': { size: 130, weight: 400, ls: 0 },
'Oswald': { size: 118, weight: 700, ls: -3 },
'Anton': { size: 122, weight: 400, ls: -3 },
'Archivo Black': { size: 100, weight: 400, ls: -5 },
'Russo One': { size: 100, weight: 400, ls: -3 },
'Big Shoulders Display': { size: 130, weight: 900, ls: -5 },
'Audiowide': { size: 92, weight: 400, ls: 0 },
'Orbitron': { size: 95, weight: 900, ls: -3 },
'Krona One': { size: 92, weight: 400, ls: 0 },
'Michroma': { size: 76, weight: 400, ls: 0 },
'Major Mono Display': { size: 92, weight: 400, ls: -3 },
'Syncopate': { size: 84, weight: 700, ls: 0 },
'Iceland': { size: 130, weight: 400, ls: -5 },
'Wallpoet': { size: 100, weight: 400, ls: 0 },
'Black Ops One': { size: 108, weight: 400, ls: -3 },
'Bungee': { size: 92, weight: 400, ls: -3 },
'Stalinist One': { size: 92, weight: 400, ls: -3 },
'Limelight': { size: 102, weight: 400, ls: -3 },
'Monoton': { size: 108, weight: 400, ls: -3 },
'Faster One': { size: 118, weight: 400, ls: -5 },
'Tangerine': { size: 170, weight: 700, ls: -5 },
'Great Vibes': { size: 140, weight: 400, ls: -5 },
'Allura': { size: 140, weight: 400, ls: -5 },
'Pinyon Script': { size: 140, weight: 400, ls: -5 },
};
const ft = fontTuning[font] || fontTuning['Bodoni Moda'];
const FS = ft.size;
const FW = ft.weight;
const LS = ft.ls;
const FAM = `"${font}", "Bodoni Moda", "Playfair Display", serif`;
// Hex grid pattern for the neural side
const hexNodes = [
{ x: 178, y: 70, r: 3.6, d: 0 },
{ x: 198, y: 55, r: 2.4, d: 0.4 },
{ x: 218, y: 75, r: 3, d: 0.8 },
{ x: 195, y: 92, r: 2.6, d: 0.2 },
{ x: 215, y: 110, r: 3.2, d: 1.0 },
{ x: 190, y: 128, r: 2.4, d: 1.3 },
{ x: 175, y: 110, r: 3, d: 0.6 },
{ x: 230, y: 95, r: 2.2, d: 1.5 },
{ x: 168, y: 88, r: 2.6, d: 1.8 },
];
const hexEdges = [
[0,1],[1,2],[2,4],[4,5],[5,6],[6,0],[0,3],[3,1],[3,4],[3,6],[2,7],[4,7],[0,8],[6,8],[3,8],
];
return (
{/* === Ambient glow — pure circular, no rectangular bounds === */}
{/* === Floating ambient particles === */}
{/* === MAIN COMPOSITION === */}
{/* === Corner brackets removed for seamless blend === */}
{/* === Diagonal shine sweep removed (no tile to shine on) === */}
);
}
// ─────────────────────────────────────────────────────────────
// Inline icons
// ─────────────────────────────────────────────────────────────
const Icons = {
shield: (c) =>
,
mail: (c) =>
,
lock: (c) =>
,
eye: (c) =>
,
eyeOff: (c) =>
,
chip: (c) =>
,
doc: (c) =>
,
chart: (c) =>
,
shieldFeat: (c) =>
,
cloud: (c) =>
};
// ─────────────────────────────────────────────────────────────
// Login Screen
// ─────────────────────────────────────────────────────────────
function LoginScreen({ tweaks, animPaused }) {
const p = PALETTES[tweaks.accent][tweaks.dark ? 'dark' : 'light'];
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [showPw, setShowPw] = useState(false);
const [remember, setRemember] = useState(true);
const [focused, setFocused] = useState(null);
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const [errors, setErrors] = useState({});
const [showForgot, setShowForgot] = useState(false);
// ─── Load remembered email ตอน mount (Option A — Auto-fill email) ───
useEffect(() => {
try {
const savedEmail = localStorage.getItem('remember_email');
if (savedEmail) {
setEmail(savedEmail);
setRemember(true);
} else {
// ถ้าไม่มี · ค่า default remember = false (ไม่บังคับ)
setRemember(false);
}
} catch (e) { /* localStorage ใช้ไม่ได้ — ignore */ }
}, []);
const blurAmt = tweaks.glass; // 0..24
const onSubmit = async (e) => {
e.preventDefault();
const errs = {};
if (!email) errs.email = 'กรุณากรอกอีเมล';
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) errs.email = 'รูปแบบอีเมลไม่ถูกต้อง';
if (!password) errs.password = 'กรุณากรอกรหัสผ่าน';
else if (password.length < 6) errs.password = 'รหัสผ่านอย่างน้อย 6 ตัวอักษร';
setErrors(errs);
if (Object.keys(errs).length) return;
setLoading(true);
try {
if (!window.api) throw new Error('API client ไม่พร้อม — รีเฟรชหน้าใหม่');
await window.api.login(email, password);
// ─── Save / clear remembered email ตามที่ ติ๊ก "จำฉันไว้" ───
try {
if (remember) localStorage.setItem('remember_email', email);
else localStorage.removeItem('remember_email');
} catch (e) { /* ignore */ }
setLoading(false);
setSuccess(true);
setTimeout(() => { window.location.href = '/static/dashboard.html'; }, 600);
} catch (err) {
setLoading(false);
setErrors({ password: err.message || 'เข้าสู่ระบบไม่สำเร็จ — ตรวจ email/password อีกครั้ง' });
}
};
const inputStyle = (key) => ({
width: '100%', boxSizing: 'border-box',
background: p.inputBg,
border: `1.5px solid ${errors[key] ? '#ef4444' : focused === key ? p.inputBorderFocus : p.inputBorder}`,
borderRadius: 14, padding: '12px 14px 12px 50px',
fontSize: 15, color: p.text,
outline: 'none', transition: 'all .18s ease',
boxShadow: focused === key ?
`0 0 0 4px ${p.inputBorderFocus}26, 0 2px 8px ${p.inputBorderFocus}15` :
'0 1px 2px rgba(0,0,0,0.03)',
fontFamily: 'inherit'
});
return (
{tweaks.showFrame && (
)}
{/* LINE webview header removed */}
{/* Scrollable content */}
{/* Logo */}
{/* AI POWERED pill */}
{Icons.chip('#0a2358')}
AI Powered Accounting
{/* Glass Login Card */}
{/* Feature row */}
{[
{ icon: '📄', label: 'จัดการเอกสาร', sub: 'อัตโนมัติด้วย AI' },
{ icon: '📊', label: 'วิเคราะห์ข้อมูล', sub: 'เชิงลึก' },
{ icon: '🛡️', label: 'ปลอดภัย', sub: 'ระดับองค์กร' },
{ icon: '☁️', label: 'ทำงานได้ทุกที่', sub: 'ทุกเวลา' }
].map((f, i) =>
{f.icon}
{f.label}
{f.sub}
)}
{/* Footer */}
© 2026 SC AccountPro · v1.0.0
Professional Accounting AI System for Thai Accountants
{/* ─── Forgot Password Modal ─────────────────────── */}
{showForgot && (
setShowForgot(false)} style={{
position: 'fixed', inset: 0, zIndex: 9999,
background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(4px)',
display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20,
animation: 'fadeIn .15s ease'
}}>
e.stopPropagation()} style={{
background: '#fff', borderRadius: 18, maxWidth: 440, width: '100%',
padding: 28, boxShadow: '0 24px 80px rgba(0,0,0,0.4)',
fontFamily: 'inherit'
}}>
🔐
ลืมรหัสผ่าน?
วิธี reset รหัสผ่านของคุณ
📞 ติดต่อ Owner (ผู้ดูแลระบบ)
แจ้ง Owner เพื่อ reset รหัสผ่านให้คุณ · Owner สามารถออกรหัสผ่านชั่วคราวให้ผ่านหน้า "จัดการผู้ใช้ (User Admin)" ได้
ขั้นตอน:
- ติดต่อ Owner / ผู้ดูแลระบบ
- Owner reset password ในหน้า User Admin
- รับรหัสผ่านชั่วคราวจาก Owner
- Login → ระบบจะให้ตั้งรหัสผ่านใหม่ทันที
🛡️ ระบบ reset ผ่าน Owner เท่านั้น เพื่อความปลอดภัยสูงสุด
(Email reset link จะมีในอัพเดตหน้า)
)}
);
}
// Export to window so App in login.html can render
Object.assign(window, { LoginScreen, PALETTES });