: as imagens em img/ são capturas do
// app rodando com dados fictícios (Estúdio Lumen). width/height são os pixels
// reais do arquivo — sem eles a página reflui quando a imagem carrega (CLS).
// `priority` marca a imagem do hero, que é o LCP da página.
function Shot({src, alt, width, height, priority=false, style={}}){
return (
{/* fetchpriority em minúsculo: o React 18 não reconhece a forma
camelCase e emite warning no console. */}
);
}
// ---- waitlist: captura de beta testers (lista de espera / newsletter) ----
// Ainda não estamos abertos ao público — todo CTA de "começar" cai aqui em vez
// de ir pro app, até o produto sair do beta fechado.
const WAITLIST_ENDPOINT = 'https://api.prumotech.com/api/waitlist';
// Botão que abre o modal de lista de espera. `source` identifica o botão/página
// de origem (ex: "hero", "pricing_escritorio") pra dar contexto no futuro CRM.
function WaitlistBtn({children, source, ...props}){
const openModal = (e)=>{
e.preventDefault();
window.dispatchEvent(new CustomEvent('prumo:waitlist-open',{detail:{source}}));
};
return {children};
}
// Campo com rótulo acima do input. O placeholder some quando o usuário digita,
// então ele nunca é o único rótulo — leitor de tela e usuário vidente precisam
// dos dois.
function Field({id, label, hint, ...inputProps}){
return (
);
}
// Modal de captura — ouve o evento global 'prumo:waitlist-open' disparado por
// qualquer WaitlistBtn da página. Renderizado uma única vez dentro do ,
// que está presente em toda página do site.
function WaitlistModal(){
const [open,setOpen]=React.useState(false);
const [source,setSource]=React.useState('');
const [status,setStatus]=React.useState('idle'); // idle | submitting | success | error
const [errorMsg,setErrorMsg]=React.useState('');
const [form,setForm]=React.useState({name:'',email:'',whatsapp:'',website:''});
const dialogRef=React.useRef(null);
const openerRef=React.useRef(null); // elemento que abriu o modal, para devolver o foco
React.useEffect(()=>{
const onOpen = (e)=>{
openerRef.current = document.activeElement;
setSource(e.detail?.source || '');
setStatus('idle'); setErrorMsg('');
setForm({name:'',email:'',whatsapp:'',website:''});
setOpen(true);
};
window.addEventListener('prumo:waitlist-open', onOpen);
return ()=>window.removeEventListener('prumo:waitlist-open', onOpen);
},[]);
// Esc fecha, Tab circula dentro do modal, o scroll do fundo trava e o foco
// volta para o botão que abriu quando fecha.
React.useEffect(()=>{
if(!open) return;
const node = dialogRef.current;
const focusables = () => node
? [...node.querySelectorAll('a[href], button:not([disabled]), input:not([tabindex="-1"]), [tabindex]:not([tabindex="-1"])')]
: [];
const onKey = (e)=>{
if(e.key==='Escape'){ setOpen(false); return; }
if(e.key!=='Tab') return;
const els = focusables();
if(!els.length) return;
const first = els[0], last = els[els.length-1];
// prende o Tab no modal: do último volta pro primeiro e vice-versa
if(e.shiftKey && document.activeElement===first){ e.preventDefault(); last.focus(); }
else if(!e.shiftKey && document.activeElement===last){ e.preventDefault(); first.focus(); }
};
document.addEventListener('keydown', onKey);
const prevOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
// foca o primeiro campo, não o "fechar", para quem abriu já poder digitar
(node?.querySelector('input:not([tabindex="-1"])') || focusables()[0])?.focus();
return ()=>{
document.removeEventListener('keydown', onKey);
document.body.style.overflow = prevOverflow;
openerRef.current?.focus?.();
};
},[open]);
if(!open) return null;
const submit = async (e)=>{
e.preventDefault();
setStatus('submitting'); setErrorMsg('');
try{
const res = await fetch(WAITLIST_ENDPOINT, {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({
name: form.name, email: form.email,
whatsapp: form.whatsapp || undefined, source, website: form.website,
}),
});
if(!res.ok){
const data = await res.json().catch(()=>({}));
throw new Error(data.message || 'Não foi possível enviar. Tente novamente.');
}
setStatus('success');
}catch(err){
setStatus('error');
setErrorMsg(err.message || 'Não foi possível enviar. Tente novamente.');
}
};
return (
Você entrou pra lista de beta testers. Avisamos assim que abrir uma vaga.
{e.preventDefault();setOpen(false);}}>Fechar
) : (
Lista de espera
Ainda em fase beta.
O Prumo ainda não está aberto ao público — estamos com um grupo fechado de
beta testers. Deixe seu contato e te chamamos assim que abrir uma vaga.
)}
);
}
// Prefixo para âncoras que vivem na landing.
// Na própria landing retorna '' (âncora na mesma página); em qualquer outra
// página (ex.: contato) retorna o arquivo da landing para o link redirecionar até lá.
function landingBase(){
const file = decodeURIComponent(location.pathname.split('/').pop() || '');
const onLanding = file === '' || file === 'index.html';
return onLanding ? '' : 'index.html';
}
// ---- top nav ----
function Nav(){
const [open,setOpen]=React.useState(false);
const base=landingBase(); // '' na landing, caminho da landing fora dela
// "Módulos" e não "Para quem é": a âncora #publico leva à grade de módulos
// ("Um módulo para cada parte do seu escritório"), não a uma seção de público.
// O id #publico fica como está para não quebrar links já compartilhados.
const links=[['Recursos','#recursos'],['Módulos','#publico'],['Preços','#precos'],['Sobre','Prumo%20-%20Sobre.html'],['Blog','Prumo%20-%20Blog.html']];
// Âncoras (#) ganham o prefixo da landing fora dela; links de arquivo ficam como estão.
const hrefFor = h => h.startsWith('#') ? base+h : h;
const close=()=>setOpen(false);
return (
Entrar
{/* "Lista de espera" e não "Começar grátis": o botão abre a lista do beta
fechado, não um cadastro. Variante curta para caber ao lado de "Entrar". */}
Lista de espera