/* global React, ReactDOM */ const { useState, useEffect, useRef, useMemo } = React; /* =========================================================== HOOK: Persistência automática via localStorage =========================================================== */ const useLocalStorage = (key, initial) => { const [state, setState] = useState(() => { try { const raw = localStorage.getItem(key); return raw !== null ? JSON.parse(raw) : initial; } catch { return initial; } }); useEffect(() => { try { localStorage.setItem(key, JSON.stringify(state)); } catch { /* Quota exceeded - silencioso */ } }, [key, state]); return [state, setState]; }; window.useLocalStorage = useLocalStorage; /* =========================================================== TWEAKS DEFAULTS =========================================================== */ const TWEAK_DEFAULTS = { "accent": "#3D8BFF", "density": "comfortable", "showCircuitry": true, "monoReadouts": true }; const ACCENT_MAP = { "#3D8BFF": { primary: "oklch(72% 0.18 245)", glow: "oklch(72% 0.22 245 / 0.35)", soft: "oklch(72% 0.18 245 / 0.12)" }, "#3DD6E6": { primary: "oklch(82% 0.14 200)", glow: "oklch(82% 0.18 200 / 0.40)", soft: "oklch(82% 0.14 200 / 0.12)" }, "#5BE38C": { primary: "oklch(84% 0.18 145)", glow: "oklch(84% 0.22 145 / 0.35)", soft: "oklch(84% 0.18 145 / 0.12)" }, "#F2B043": { primary: "oklch(80% 0.16 75)", glow: "oklch(80% 0.20 75 / 0.35)", soft: "oklch(80% 0.16 75 / 0.12)" }, }; /* =========================================================== ICONS — minimal stroke icons =========================================================== */ const Dropdown = ({ value, onChange, options }) => { const [open, setOpen] = useState(false); const ref = useRef(null); useEffect(() => { if (!open) return; const onDocClick = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; document.addEventListener("mousedown", onDocClick); return () => document.removeEventListener("mousedown", onDocClick); }, [open]); const current = options.find(o => o.value === value) || options[0]; return (
{open && (
{options.map(o => (
{ onChange(o.value); setOpen(false); }} >{o.label}
))}
)}
); }; const Icon = ({ name, size = 18, stroke = 1.5 }) => { const common = { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: stroke, strokeLinecap: "round", strokeLinejoin: "round" }; const paths = { dashboard: <>, calc: <>, ruler: <>, bolt: <>, wave: <>, pipe: <>, cable: <>, flash: <>, shield: <>, transformer: <>, sigma: <>, chart: <>, bulb: <>, diagram: <>, refresh: <>, settings: <>, arrow: <>, arrowL: <>, check: <>, plus: <>, user: <>, search: <>, bell: <>, download: <>, spark: <>, grid: <>, lock: <>, mail: <>, eye: <>, }; return {paths[name] || null}; }; /* =========================================================== BACKGROUND — animated circuitry SVG layer =========================================================== */ const Circuitry = ({ show }) => { if (!show) return null; return ( ); }; /* =========================================================== BRAND MARK — logo "V" (volt) bicolor, segue o --accent do tema =========================================================== */ const BrandMark = ({ size = 28 }) => ( ); window.VL_BrandMark = BrandMark; /* =========================================================== LANDING / LOGIN / CADASTRO =========================================================== */ const Landing = ({ onEnter, onGuest, showCircuitry }) => { const [pwVisible, setPwVisible] = useState(false); const [mode, setMode] = useState("login"); // "login" | "register" | "verify" | "forgot" const [email, setEmail] = useState(""); const [senha, setSenha] = useState(""); const [nome, setNome] = useState(""); const [codigo, setCodigo] = useState(""); const [manterSessao, setManterSessao] = useState(true); const [erro, setErro] = useState(""); const [busy, setBusy] = useState(false); const fpRef = useRef(""); useEffect(() => { if (window.VL_fingerprint) { window.VL_fingerprint().then(fp => { fpRef.current = fp; }); } }, []); const concluirLogin = (res) => { window.VL_AUTH.setToken(res.token, manterSessao); window.VL_SESSION = { token: res.token, user: res.user, plan: res.plan, fingerprint: fpRef.current }; onEnter(); }; const handleLogin = async (e) => { e.preventDefault(); setErro(""); if (!window.VL_AUTH) { onEnter(); return; } if (!email || !senha) { setErro("Preencha e-mail e senha."); return; } setBusy(true); const res = await window.VL_AUTH.login({ email: email.trim().toLowerCase(), senha, fingerprint: fpRef.current, remember: manterSessao, }); setBusy(false); if (!res.ok) { setErro(res.error); return; } concluirLogin(res); }; const handleRegister = async (e) => { e.preventDefault(); setErro(""); if (!window.VL_AUTH) { onEnter(); return; } if (!nome.trim()) { setErro("Informe seu nome completo."); return; } if (!email.includes("@")) { setErro("E-mail inválido."); return; } if (senha.length < 6) { setErro("A senha deve ter ao menos 6 caracteres."); return; } setBusy(true); const emailNorm = email.trim().toLowerCase(); const res = await window.VL_AUTH.requestRegistrationCode({ nome: nome.trim(), email: emailNorm, senha }); setBusy(false); if (!res.ok) { setErro(res.error); return; } alert(`VoltLab — Verificação de conta\n\nEnviamos um código de confirmação para ${emailNorm}.\n\nSeu código é: ${res.codeForTesting}\n\nDigite-o na próxima tela para concluir o cadastro.`); setMode("verify"); }; const handleVerify = async (e) => { e.preventDefault(); setErro(""); if (!codigo) { setErro("Informe o código de 6 dígitos."); return; } setBusy(true); const emailNorm = email.trim().toLowerCase(); const reg = await window.VL_AUTH.verifyAndRegister({ email: emailNorm, code: codigo }); if (!reg.ok) { setBusy(false); setErro(reg.error); return; } const res = await window.VL_AUTH.login({ email: emailNorm, senha, fingerprint: fpRef.current, remember: manterSessao }); setBusy(false); if (!res.ok) { setErro(res.error); return; } concluirLogin(res); }; const handleForgot = (e) => { e.preventDefault(); alert("Se o e-mail estiver cadastrado, um link de recuperação foi enviado."); setMode("login"); }; return (