/* global React */ (function () { const { useState, useEffect } = React; // ============ Router (hash-based) ============ function useRoute() { const get = () => (location.hash.replace(/^#\/?/, '') || 'home').split('?')[0]; const [route, setRoute] = useState(get); useEffect(() => { const on = () => { setRoute(get()); window.scrollTo({ top: 0, behavior: 'instant' }); }; window.addEventListener('hashchange', on); return () => window.removeEventListener('hashchange', on); }, []); return route; } function navigate(route) { location.hash = '/' + route; } // ============ Theme ============ function useTheme() { const [theme, setTheme] = useState(() => { try { return localStorage.getItem('pc-theme') || 'light'; } catch { return 'light'; } }); useEffect(() => { document.documentElement.setAttribute('data-theme', theme); try { localStorage.setItem('pc-theme', theme); } catch {} }, [theme]); return [theme, setTheme]; } // ============ Nav ============ function Nav({ route }) { const [theme, setTheme] = window.useTheme(); const items = [ { id: 'home', label: 'Início' }, { id: 'servicos', label: 'Serviços' }, { id: 'sobre', label: 'Sobre' }, { id: 'contato', label: 'Contato' }, ]; return ( ); } // ============ Footer ============ function Footer() { return ( ); } // ============ Reveal-on-scroll wrapper ============ function Reveal({ children, delay = 0, as: As = 'div', className = '', ...rest }) { const ref = React.useRef(null); useEffect(() => { const el = ref.current; if (!el) return; const obs = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { setTimeout(() => e.target.classList.add('in'), delay); obs.unobserve(e.target); } }); }, { threshold: 0.12 }); obs.observe(el); return () => obs.disconnect(); }, [delay]); return {children}; } // ============ Placeholder image ============ function Placeholder({ label, height = '100%', stripe = 'diag', tone = 'ink' }) { // tone: 'ink' (neutral) or 'orange' const stripeColor = tone === 'orange' ? 'color-mix(in oklab, var(--orange) 22%, transparent)' : 'color-mix(in oklab, var(--ink) 8%, transparent)'; const bgTint = tone === 'orange' ? 'color-mix(in oklab, var(--orange) 6%, var(--bg-elev))' : 'color-mix(in oklab, var(--ink) 3%, var(--bg-elev))'; const angle = stripe === 'reverse' ? 45 : 135; return (
{label}
); } // ============ Animated counter ============ function Counter({ value, suffix = '', prefix = '', duration = 1600 }) { const ref = React.useRef(null); const [shown, setShown] = useState('0'); useEffect(() => { const el = ref.current; if (!el) return; let started = false; const obs = new IntersectionObserver((entries) => { if (started || !entries[0].isIntersecting) return; started = true; const start = performance.now(); const isFloat = /\./.test(String(value)); const target = parseFloat(value); function tick(now) { const t = Math.min(1, (now - start) / duration); const eased = 1 - Math.pow(1 - t, 3); const cur = target * eased; setShown(isFloat ? cur.toFixed(1) : Math.round(cur).toLocaleString('pt-BR')); if (t < 1) requestAnimationFrame(tick); else setShown(value.toLocaleString ? value.toLocaleString('pt-BR') : String(value)); } requestAnimationFrame(tick); }, { threshold: 0.4 }); obs.observe(el); return () => obs.disconnect(); }, [value, duration]); return {prefix}{shown}{suffix}; } // ============ BIM Comparator (real drag) ============ function Compare({ heightPx = 520 }) { const ref = React.useRef(null); const imgRef = React.useRef(null); const [pos, setPos] = useState(100); const dragging = React.useRef(false); const animated = React.useRef(false); const minPosRef = React.useRef(0); // Calculates the minimum bar position: the point where the right image // exactly fills the visible area with no striped background showing. const calcMinPos = () => { const container = ref.current; const img = imgRef.current; if (!container || !img || !img.naturalWidth) return; const cw = container.getBoundingClientRect().width; const imgAspect = img.naturalWidth / img.naturalHeight; const boxAspect = cw / heightPx; const renderedW = imgAspect <= boxAspect ? heightPx * imgAspect : cw; const newMin = Math.max(0, (1 - renderedW / cw) * 100); minPosRef.current = newMin; setPos(p => Math.max(p, newMin)); }; const move = (clientX) => { const el = ref.current; if (!el) return; const r = el.getBoundingClientRect(); const p = Math.max(minPosRef.current, Math.min(100, ((clientX - r.left) / r.width) * 100)); setPos(p); }; // Auto-animate on first viewport entry to hint interactivity useEffect(() => { const el = ref.current; if (!el) return; const obs = new IntersectionObserver((entries) => { if (animated.current || !entries[0].isIntersecting) return; animated.current = true; obs.unobserve(el); // Animate within the valid range [minPos, 100] // Targets as fractions of the range above minPos const rawTargets = [0.05, 0.65, 0.4]; const durations = [1100, 950, 800]; let cur = 100; let step = 0; function runStep() { if (step >= rawTargets.length || dragging.current) return; const range = 100 - minPosRef.current; const target = minPosRef.current + rawTargets[step] * range; const duration = durations[step]; const from = cur; const t0 = performance.now(); function frame(now) { if (dragging.current) return; const t = Math.min(1, (now - t0) / duration); const e = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; cur = from + (target - from) * e; setPos(cur); if (t < 1) requestAnimationFrame(frame); else { cur = target; setPos(target); step++; setTimeout(runStep, 200); } } requestAnimationFrame(frame); } setTimeout(runStep, 700); }, { threshold: 0.35 }); obs.observe(el); return () => obs.disconnect(); }, []); useEffect(() => { window.addEventListener('resize', calcMinPos); return () => window.removeEventListener('resize', calcMinPos); }, []); useEffect(() => { const onMove = (e) => { if (dragging.current) move(e.clientX ?? (e.touches?.[0]?.clientX)); }; const onUp = () => { dragging.current = false; }; window.addEventListener('mousemove', onMove); window.addEventListener('mouseup', onUp); window.addEventListener('touchmove', onMove); window.addEventListener('touchend', onUp); return () => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); window.removeEventListener('touchmove', onMove); window.removeEventListener('touchend', onUp); }; }, []); return (
{ dragging.current = true; move(e.clientX); }} onTouchStart={(e) => { dragging.current = true; move(e.touches[0].clientX); }} style={{ height: heightPx }} >
Obra de referência Modelo 3D · BIM
Render arquitetônico
Orçamento de referência
Quantitativos extraídos
→ Planilha orçamentária
arraste ← →
); } // ============ Cookie banner (LGPD) ============ function CookieBanner() { const [shown, setShown] = useState(false); const [expanded, setExpanded] = useState(false); const [prefs, setPrefs] = useState({ necessary: true, analytics: false }); useEffect(() => { let stored; try { stored = localStorage.getItem('pc-cookies'); } catch {} if (!stored) setShown(true); }, []); const persist = (choice) => { try { localStorage.setItem('pc-cookies', JSON.stringify({ ...choice, ts: Date.now() })); } catch {} setShown(false); }; if (!shown) return null; return (
⚲ LGPD · cookies

Usamos cookies para fazer este site funcionar.

Cookies estritamente necessários estão sempre ativos. Para cookies analíticos (estatísticas anônimas de uso), precisamos da sua autorização — conforme a Lei nº 13.709/2018. Leia nossa { e.preventDefault(); persist({ necessary: true, analytics: false }); navigate('privacidade'); }} href="#/privacidade">Política de Privacidade.

{expanded && (
)}
{!expanded ? ( <> ) : ( <> )}
); } // ============ WhatsApp button ============ function WhatsAppButton() { const phone = '5531988817140'; const message = encodeURIComponent('Olá! Gostaria de solicitar um orçamento.'); return ( { e.currentTarget.style.transform = 'scale(1.1)'; e.currentTarget.style.boxShadow = '0 6px 20px rgba(0,0,0,0.24)'; }} onMouseLeave={e => { e.currentTarget.style.transform = 'scale(1)'; e.currentTarget.style.boxShadow = '0 4px 16px rgba(0,0,0,0.18)'; }} > ); } // Expose everything Object.assign(window, { useRoute, navigate, useTheme, Nav, Footer, Reveal, Placeholder, Counter, Compare, CookieBanner, WhatsAppButton }); })();