/* ============================================================ Sylntra website — shared React components. Exposes window.SS.* — load AFTER React, ReactDOM, _ds_bundle.js and image-slot.js. ============================================================ */ (function () { const DS = window.SylntraDesignSystem_b1b938; const { Button, IconButton, WordmarkBanner } = DS; const h = React.createElement; /* ---- Site config ---------------------------------------- */ const NAV = [ { label: 'Home', href: 'index.html' }, { label: 'Services', href: 'services.html' }, { label: 'About', href: 'about.html' }, { label: 'Blog', href: 'blog.html' }, { label: 'Contact', href: 'contact.html' }, ]; const FOOTER_COLS = [ { heading: 'Services', links: [ ['Website Development', 'services.html'], ['AI Automation', 'services.html'], ['Business Systems', 'services.html'], ['Workflow Optimization', 'services.html'], ['BI Dashboards', 'services.html'], ['Consulting', 'services.html'] ] }, { heading: 'Company', links: [ ['About', 'about.html'], ['Our Approach', 'about.html'], ['Process', 'about.html'], ['Careers', 'about.html'] ] }, { heading: 'Resources', links: [ ['Blog', 'blog.html'], ['Insights', 'blog.html'], ['Case Studies', 'blog.html'], ['FAQs', 'index.html'] ] }, { heading: 'Connect', links: [ ['Book a consult', 'contact.html'], ['Contact', 'contact.html'], ['Email us', 'contact.html'] ] }, ]; /* ---- Icons (Lucide-style, 2px stroke) ------------------- */ function Icon({ d, paths, size = 24, fill = false, stroke = 'currentColor', sw = 1.6, style }) { return h('svg', { width: size, height: size, viewBox: '0 0 24 24', fill: fill ? 'currentColor' : 'none', stroke: stroke, strokeWidth: sw, strokeLinecap: 'round', strokeLinejoin: 'round', style, 'aria-hidden': true }, paths ? paths.map((p, i) => h('path', { key: i, d: p })) : h('path', { d })); } const ICONS = { arrowRight: 'M5 12h14M13 6l6 6-6 6', arrowUpRight: 'M7 17 17 7M8 7h9v9', check: 'M20 6 9 17l-5-5', spark: ['M12 3v4M12 17v4M3 12h4M17 12h4M5.6 5.6l2.8 2.8M15.6 15.6l2.8 2.8M18.4 5.6l-2.8 2.8M8.4 15.6l-2.8 2.8'], layers: ['M12 2 2 7l10 5 10-5-10-5z', 'm2 17 10 5 10-5', 'm2 12 10 5 10-5'], workflow: ['M3 3h7v7H3zM14 14h7v7h-7z', 'M10 6.5h4a3 3 0 0 1 3 3V14'], gauge: ['M12 14 4 6', 'M12 22a10 10 0 1 0-10-10'], bot: ['M12 8V4H8', 'M2 14h2M20 14h2M15 13v2M9 13v2', 'M4 8h16a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2z'], compass: ['M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z', 'm16.2 7.8-2.9 6.3-6.3 2.9 2.9-6.3 6.3-2.9z'], target: ['M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z', 'M12 18a6 6 0 1 0 0-12 6 6 0 0 0 0 12z', 'M12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4z'], clock: ['M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z', 'M12 6v6l4 2'], trend: ['m23 6-9.5 9.5-5-5L1 18', 'M17 6h6v6'], shield: ['M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z'], users: ['M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2', 'M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z', 'M22 21v-2a4 4 0 0 0-3-3.87', 'M16 3.13a4 4 0 0 1 0 7.75'], search: ['M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z', 'm21 21-4.3-4.3'], mail: ['M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z', 'm22 7-10 6L2 7'], pin: ['M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0z', 'M12 12a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z'], chat: ['M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z'], quote: ['M6 17h3l2-4V7H5v6h3zM16 17h3l2-4V7h-6v6h3z'], play: ['M6 4l14 8-14 8z'], cube: ['M12 2 2 7l10 5 10-5-10-5z', 'M2 17l10 5 10-5M2 12l10 5 10-5'], }; function Glyph({ name, size = 24, sw = 1.6, style }) { const p = ICONS[name]; if (!p) return null; return Array.isArray(p) ? h(Icon, { paths: p, size, sw, style }) : h(Icon, { d: p, size, sw, style }); } /* ---- Reveal-on-scroll ----------------------------------- Primary: IntersectionObserver (nice on-approach reveal in real browsers). Plus an immediate rect check for above-the-fold, plus an unconditional fallback so non-scrolling embeds / backgrounded tabs (where IO + CSS transitions stall) never leave content invisible. --- */ function _lock(el) { el.style.transition = 'none'; el.style.opacity = '1'; el.style.transform = 'none'; } function Reveal({ children, variant = 'reveal', delay = 0, as = 'div', className = '', style = {}, ...rest }) { const ref = React.useRef(null); React.useEffect(() => { const el = ref.current; if (!el) return; let done = false, io, t, lt; const reveal = () => { if (done) return; done = true; el.classList.add('is-in'); if (io) io.disconnect(); clearTimeout(t); lt = setTimeout(() => _lock(el), delay + 900); }; try { io = new IntersectionObserver((ents) => { ents.forEach((e) => { if (e.isIntersecting) reveal(); }); }, { threshold: 0.1, rootMargin: '0px 0px -6% 0px' }); io.observe(el); } catch (e) { /* no IO */ } const r = el.getBoundingClientRect(); const vh = window.innerHeight || document.documentElement.clientHeight; if (r.top < vh * 0.92 && r.bottom > 0) reveal(); else t = setTimeout(reveal, 1500); return () => { if (io) io.disconnect(); clearTimeout(t); clearTimeout(lt); }; }, []); return h(as, { ref, className: `${variant} ${className}`.trim(), style: { transitionDelay: delay ? `${delay}ms` : undefined, ...style }, ...rest }, children); } /* ---- Count-up ------------------------------------------- */ function Counter({ to, suffix = '', prefix = '', decimals = 0, duration = 1600, className = '', style = {} }) { const ref = React.useRef(null); const [val, setVal] = React.useState(0); React.useEffect(() => { const el = ref.current; if (!el) return; let raf, started = false, io, t; const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches; const animate = () => { if (started) return; started = true; if (io) io.disconnect(); clearTimeout(t); if (reduce) { setVal(to); return; } const t0 = performance.now(); const tick = (now) => { const p = Math.min(1, (now - t0) / duration); setVal(to * (1 - Math.pow(1 - p, 3))); if (p < 1) raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); }; const finalNow = () => { if (started) return; started = true; if (io) io.disconnect(); setVal(to); }; try { io = new IntersectionObserver((ents) => { ents.forEach((e) => { if (e.isIntersecting) animate(); }); }, { threshold: 0.4 }); io.observe(el); } catch (e) { /* no IO */ } const r = el.getBoundingClientRect(); const vh = window.innerHeight || document.documentElement.clientHeight; if (r.top < vh * 0.85 && r.bottom > 0) animate(); else t = setTimeout(finalNow, 1700); // non-scrolling embeds: show final value return () => { if (io) io.disconnect(); cancelAnimationFrame(raf); clearTimeout(t); }; }, [to]); const shown = decimals ? val.toFixed(decimals) : Math.round(val).toLocaleString(); return h('span', { ref, className, style }, `${prefix}${shown}${suffix}`); } /* ---- Image slot wrapper --------------------------------- */ function ImageSlot({ id, placeholder = 'Drop an image', shape = 'rounded', radius = 8, ratio, fit, src, className = '', style = {} }) { const st = { width: '100%', ...style }; if (ratio) st.aspectRatio = ratio; return h('image-slot', { id, placeholder, shape, radius: String(radius), fit, src, class: className, style: st }); } /* ---- Header --------------------------------------------- */ function SiteHeader({ active }) { const [solid, setSolid] = React.useState(false); const [open, setOpen] = React.useState(false); React.useEffect(() => { const onScroll = () => setSolid(window.scrollY > 40); onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); return () => window.removeEventListener('scroll', onScroll); }, []); React.useEffect(() => { document.body.style.overflow = open ? 'hidden' : ''; }, [open]); const txt = solid ? 'var(--color-ink)' : 'var(--color-on-dark)'; return h(React.Fragment, null, h('header', { className: `site-header ${solid ? 'is-solid' : 'is-top'}` }, h('div', { className: 'container site-header__inner' }, h('a', { className: 'brand-lockup', href: 'index.html', style: { color: txt } }, h('span', { className: 'brand-mark' }), h('span', { className: 'brand-word' }, 'sylntra')), h('nav', { className: 'nav-links' }, NAV.map((n) => h('a', { key: n.label, href: n.href, className: `nav-link ${active === n.label ? 'is-active' : ''}`, style: { color: txt } }, n.label))), h('div', { className: 'nav-actions' }, h('a', { href: 'contact.html', className: 'nav-secondary nav-link', style: { color: txt } }, 'Book a consult'), h('span', { className: 'nav-cta-desktop' }, h(Button, { variant: 'primary', href: 'contact.html' }, 'Start a project')), h('button', { className: 'nav-toggle', 'aria-label': 'Menu', onClick: () => setOpen(o => !o) }, h('span', { style: { background: txt } }))) )), h('div', { className: `mobile-drawer ${open ? 'is-open' : ''}` }, NAV.map((n) => h('a', { key: n.label, href: n.href, onClick: () => setOpen(false) }, n.label)), h('div', { style: { marginTop: 'var(--space-3xl)' } }, h(Button, { variant: 'secondary-mint', href: 'contact.html' }, 'Start a project'))) ); } /* ---- Footer --------------------------------------------- */ const SOCIALS = [ ['M16 8a6 6 0 0 1 6 6v6h-4v-6a2 2 0 0 0-4 0v6h-4v-6a6 6 0 0 1 6-6zM6 9H2v11h4zM4 6a2 2 0 1 0 0-4 2 2 0 0 0 0 4z'], ['M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z'], ['M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z', 'm22 7-10 6L2 7'], ['M12 2a10 10 0 0 0-3.16 19.49c.5.09.68-.22.68-.48v-1.7c-2.78.6-3.37-1.34-3.37-1.34-.45-1.16-1.1-1.47-1.1-1.47-.91-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.9 1.52 2.34 1.08 2.91.83.09-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.94 0-1.09.39-1.98 1.03-2.68-.1-.25-.45-1.27.1-2.65 0 0 .84-.27 2.75 1.02a9.56 9.56 0 0 1 5 0c1.91-1.29 2.75-1.02 2.75-1.02.55 1.38.2 2.4.1 2.65.64.7 1.03 1.59 1.03 2.68 0 3.84-2.34 4.69-4.57 4.94.36.31.68.92.68 1.85v2.74c0 .27.18.58.69.48A10 10 0 0 0 12 2z'], ]; function SiteFooter() { return h('footer', { className: 'site-footer' }, h('div', { className: 'container band-tight' }, h('div', { className: 'footer-top' }, h('div', { className: 'footer-col footer-brandcol', style: { gap: 'var(--space-lg)' } }, h('a', { className: 'brand-lockup', href: 'index.html', style: { color: '#fff' } }, h('span', { className: 'brand-mark' }), h('span', { className: 'brand-word' }, 'sylntra')), h('p', { className: 'body', style: { color: '#9a9ab0', maxWidth: 280 } }, 'Building intelligent business systems. We don\u2019t sell technology \u2014 we solve business problems.'), h(Button, { variant: 'secondary-mint', href: 'contact.html' }, 'Book a consult')), FOOTER_COLS.map((c) => h('div', { className: 'footer-col', key: c.heading }, h('h4', null, c.heading), c.links.map((l) => h('a', { key: l[0], href: l[1] }, l[0]))))), h('div', { className: 'footer-bottom' }, h('span', { className: 'body', style: { color: '#7c7c92' } }, `\u00A9 ${new Date().getFullYear()} Sylntra \u00B7 Building Intelligent Business Systems`), h('div', { className: 'social-row' }, SOCIALS.map((p, i) => h('a', { key: i, href: '#', className: 'social-btn', 'aria-label': 'social' }, h(Icon, { paths: p, size: 18, sw: 1.5 })))), h('div', { style: { display: 'flex', gap: 'var(--space-lg)' } }, h('a', { href: '#', className: 'footer-col', style: { color: '#7c7c92', fontSize: 14 } }, 'Privacy'), h('a', { href: '#', className: 'footer-col', style: { color: '#7c7c92', fontSize: 14 } }, 'Terms')))), h('div', { style: { overflow: 'hidden', textAlign: 'center', lineHeight: 0.82, paddingTop: 'var(--space-2xl)' } }, h('div', { style: { fontFamily: 'var(--font-display)', fontWeight: 'var(--weight-medium)', fontSize: 'clamp(80px, 22vw, 300px)', letterSpacing: '-0.04em', color: '#11112e', whiteSpace: 'nowrap' } }, 'sylntra')) ); } function ChatOrb() { return h('div', { className: 'chat-orb' }, h(IconButton, { label: 'Chat with Sylntra' }, h(Glyph, { name: 'chat', size: 20, sw: 1.8 }))); } window.SS = { h, Glyph, Icon, Reveal, Counter, ImageSlot, SiteHeader, SiteFooter, ChatOrb, NAV, ICONS, DS }; })();