// type-clean.jsx — Direction D: Type · Clean // Minimal, typography-first. No boxes, no borders, no decoration — // just type, whitespace, and a single restrained accent. // // Asset paths use window.ASSETS_BASE if set (so the WordPress theme can // inject the theme dir URL) and fall back to relative './assets/'. const __ASSETS = (typeof window !== 'undefined' && window.ASSETS_BASE) || 'assets/'; const typeCleanTokens = { bg: '#f4f1ea', ink: '#1b1b1a', muted: 'rgba(27,27,26,0.5)', faint: 'rgba(27,27,26,0.22)', accent: '#ff5a1f', }; function TypeCleanArtboard({ tweaks }) { const rootRef = React.useRef(null); // Initial language: server-injected (PHP geo-detect), else cookie, else navigator.language. // Italians get 'it', everyone else gets 'en'. const [lang, setLangState] = React.useState(() => { if (typeof window === 'undefined') return 'it'; if (window.INITIAL_LANG === 'it' || window.INITIAL_LANG === 'en') return window.INITIAL_LANG; try { const m = document.cookie.match(/(?:^|; )bf_lang=([^;]+)/); if (m && (m[1] === 'it' || m[1] === 'en')) return m[1]; } catch (e) {} const nav = (navigator && navigator.language ? navigator.language : 'en').toLowerCase(); return nav.startsWith('it') ? 'it' : 'en'; }); const setLang = React.useCallback((next) => { setLangState(next); try { document.cookie = 'bf_lang=' + next + ';path=/;max-age=31536000;samesite=lax'; } catch (e) {} }, []); // Keep in sync so screen readers / search engines reflect the visible copy. React.useEffect(() => { if (typeof document !== 'undefined') { document.documentElement.lang = lang; } }, [lang]); const c = COPY[lang]; // Detect touch / coarse-pointer devices once — we suppress the custom // cursor entirely on phones/tablets (it was rendering as a non-round // shape on mobile and lingering during scroll). const isTouch = React.useMemo(() => { if (typeof window === 'undefined') return false; try { return window.matchMedia('(hover: none), (pointer: coarse)').matches || ('ontouchstart' in window); } catch (e) { return false; } }, []); const Cursor = useArtboardCursor(rootRef, { color: '#ffffff', mixBlend: 'difference' }); // Mobile drawer (hamburger menu) const [menuOpen, setMenuOpen] = React.useState(false); React.useEffect(() => { if (!menuOpen) return undefined; const onKey = (e) => { if (e.key === 'Escape') setMenuOpen(false); }; document.addEventListener('keydown', onKey); const prevOverflow = document.body.style.overflow; document.body.style.overflow = 'hidden'; return () => { document.removeEventListener('keydown', onKey); document.body.style.overflow = prevOverflow; }; }, [menuOpen]); // Hide cursor while scrolling, and hide the topbar when scrolling down // (revealing it again when the user scrolls back up). Native browser-chrome // feel — keeps reading distractions out of the way. const [showTopbar, setShowTopbar] = React.useState(true); React.useEffect(() => { let lastY = window.scrollY; let t = 0; const onScroll = () => { const y = window.scrollY; const dy = y - lastY; const root = rootRef.current; if (root && !isTouch) { root.dataset.scrolling = '1'; clearTimeout(t); t = setTimeout(() => { if (rootRef.current) rootRef.current.dataset.scrolling = '0'; }, 220); } // Topbar visibility — small dead-zone so micro-scrolls don't flicker if (Math.abs(dy) > 6) { if (y < 80) setShowTopbar(true); else if (dy > 0) setShowTopbar(false); else setShowTopbar(true); lastY = y; } }; window.addEventListener('scroll', onScroll, { passive: true }); return () => { window.removeEventListener('scroll', onScroll); clearTimeout(t); }; }, [isTouch]); // Smooth scroll helper for in-page anchors (drawer + nav) const scrollToId = React.useCallback((id) => { const el = document.getElementById(id); if (!el) return; const top = window.scrollY + el.getBoundingClientRect().top - 8; try { window.scrollTo({ top, behavior: 'smooth' }); } catch (e) { window.scrollTo(0, top); } }, []); const scrollToTop = React.useCallback(() => { setMenuOpen(false); try { window.scrollTo({ top: 0, behavior: 'smooth' }); } catch (e) { window.scrollTo(0, 0); } }, []); // Nav-item → section id mapping (parallel to c.nav) const NAV_IDS = ['work', 'philosophy', 'services', 'faq', 'contact']; const heroVisual = tweaks?.cleanHero === 'visual'; const pal = tweaks?.cleanPalette || [typeCleanTokens.bg, typeCleanTokens.ink, typeCleanTokens.accent]; const [bg, ink, accent] = pal; const displayFont = tweaks?.cleanDisplay || '"Instrument Serif", Georgia, serif'; const sansFont = tweaks?.cleanSans || '"Space Grotesk", system-ui, sans-serif'; const monoFont = '"JetBrains Mono", monospace'; const muted = `color-mix(in oklab, ${ink} 50%, transparent)`; const faint = `color-mix(in oklab, ${ink} 18%, transparent)`; // hover-anim ref for case list const [openCase, setOpenCase] = React.useState(null); const [openFaq, setOpenFaq] = React.useState(0); const [openStack, setOpenStack] = React.useState(false); const [contactOpen, setContactOpen] = React.useState(false); const [activePhase, setActivePhase] = React.useState(0); const phasesRef = React.useRef(null); // Compute active phase from scroll position. The section is sized so each // phase gets one viewport of scroll; while pinned, the active index advances. React.useEffect(() => { const onScroll = () => { const sec = phasesRef.current; if (!sec) return; const rect = sec.getBoundingClientRect(); const total = sec.offsetHeight - window.innerHeight; if (total <= 0) { setActivePhase(0); return; } const progress = Math.max(0, Math.min(1, -rect.top / total)); const n = c.phases.length; const idx = Math.max(0, Math.min(n - 1, Math.floor(progress * n))); setActivePhase(idx); }; window.addEventListener('scroll', onScroll, { passive: true }); onScroll(); return () => window.removeEventListener('scroll', onScroll); }, [c.phases.length]); const contactRef = React.useRef(null); // When the contact drawer opens, scroll it into view so the user lands // on the new content (instead of having it appear off-screen below the hero). React.useEffect(() => { if (!contactOpen || !contactRef.current) return; const id = setTimeout(() => { if (!contactRef.current) return; const rect = contactRef.current.getBoundingClientRect(); const top = window.scrollY + rect.top - 8; try { window.scrollTo({ top, behavior: 'smooth' }); } catch (e) { window.scrollTo(0, top); } }, 120); return () => clearTimeout(id); }, [contactOpen]); // simulated "now" time for the meta strip const [now, setNow] = React.useState(() => new Date()); React.useEffect(() => { const t = setInterval(() => setNow(new Date()), 60000); return () => clearInterval(t); }, []); const timeStr = now.toLocaleTimeString(lang === 'it' ? 'it-IT' : 'en-GB', { hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Rome' }); return (
{/* TOP META BAR -- sticky, hides on scroll-down, returns on scroll-up. Single-row layout on desktop: logo · nav · LinkedIn · IT/EN. */}
{/* Desktop inline nav */}
LinkedIn
{/* Mobile: LinkedIn text + Menu text — same mono style as desktop link */}
LinkedIn
{/* MOBILE DRAWER — fullscreen, drops from top. Shares the same topbar layout so the page logo never appears twice. */} {/* HERO ---------------------------------------------------------- */}
{/* Fullscreen gradient background — subtle moving blobs in brand colors */}
{/* CONTACT DRAWER — si apre dalla CTA hero ---------------------- */}
{lang === 'it' ? 'Sentiamoci' : 'Get in touch'}

{c.contactDrawerTitle}

{c.contactDrawerBody}

{ e.currentTarget.style.paddingLeft = '20px'; }} onMouseLeave={(e) => { e.currentTarget.style.paddingLeft = '0px'; }}>
01 · {lang === 'it' ? 'asincrono' : 'asynchronous'}

{c.contactMailLabel}.

{c.contactMailSub}
{ e.currentTarget.style.paddingLeft = '52px'; }} onMouseLeave={(e) => { e.currentTarget.style.paddingLeft = '32px'; }}>
02 · {lang === 'it' ? 'sincrono' : 'synchronous'}

{c.contactCalLabel}.

{c.contactCalSub}
{/* PHILOSOPHY — single quote + portrait ---------------------- */}
{c.philosophyKicker.replace(/^\d+\s*—\s*/, '')}

{lang === 'it' ? ( <>Vengo da un mondo dove se il sistema sbaglia,
si ferma qualcosa di vero. Non da slide. ) : ( <>I come from a world where if the system fails,
something real stops. Not from slides. )}

{c.philosophyP1}

{c.philosophyP2}

{/* Foto Bart — placeholder coerente: ritratto B/N, da sostituire */}
FIG. 01RITRATTO · B/N
[ drop · foto Bart al lavoro ]
luce naturale, B/N, non in giacca
Bartolomeo Tiralongo · aka Bart

{/* PATH — logos del percorso. Scorrono come marquee. -------------- */}
{c.pathKicker}
{[...c.pathLogos, ...c.pathLogos].map((p, i) => (
{p.name}
{p.sub}
))}

{/* PHASES — sticky scroll: 4 fasi raccontate come capitoli editoriali --- */}
{/* Title + sub */}

{c.phasesTitle}{' '} {c.phasesTitleAccent}.

{c.phasesSub}

{/* Strip — typografica, niente icone. Linea sottile + 4 markers cliccabili. */}
{/* Base line */}
{/* Active fill */}
1 ? (activePhase / (c.phases.length - 1)) : 0})`, height: 1, background: accent, zIndex: 0, transition: 'width .5s cubic-bezier(.2,.7,.3,1)', }} /> {c.phases.map((p, i) => { const isActive = i === activePhase; const isPast = i < activePhase; return ( ); })}
{/* Editorial content — no borders, type-led. Cards stacked, only active visible. */}
{c.phases.map((p, i) => { const offset = i - activePhase; const isActive = offset === 0; return (
{/* Left column — phase number + time */}
/ 0{i + 1}
{p.when}
{/* Right column — title + body + bullets */}

{p.t}.

{p.d}

    {p.bullets.map((b, j) => (
  • 0{j + 1} {b}
  • ))}
); })}
{/* SERVICES — three pillars (Decisioni / Architettura / Persone) -- */}
{c.servicesKicker.replace(/^\d+\s*—\s*/, '')}

{c.servicesIntro}

{c.servicePillars.map((pillar, pi) => (
— 0{pi + 1} {lang === 'it' ? 'pilastro' : 'pillar'}

{pillar.label}.

{pillar.gloss}

{pillar.services.map((s, si) => (
{ e.currentTarget.style.paddingLeft = '16px'; }} onMouseLeave={(e) => { e.currentTarget.style.paddingLeft = '0px'; }}>

{s.t}

{s.d}

))}
))}

{/* WORK — long list, hover-reveal detail ----------------------- */}
{c.workKicker.replace(/^\d+\s*—\s*/, '')}

{c.workIntro}

{c.cases.filter(cs => !cs.minor).map((cs, i, arr) => { const isOpen = openCase === i; return (
setOpenCase(isOpen ? null : i)} style={{ padding: '40px 0', borderTop: `1px solid ${faint}`, borderBottom: i === arr.length - 1 ? `1px solid ${faint}` : 'none', cursor: 'none', }}>
/{String(i + 1).padStart(2, '0')}

{cs.client.split('·')[0].trim()} {cs.client.includes('·') && ( · {cs.client.split('·')[1].trim()} )}

{cs.role}

{cs.title}

{cs.body}

{[[cs.stat1, cs.stat1l], [cs.stat2, cs.stat2l], [cs.stat3, cs.stat3l]].map(([v, l], j) => (
{v}
{l}
))}
{cs.client.startsWith('BRUM') && (
BRUM Patenti — homepage della piattaforma live
BRUM Patenti — {lang === 'it' ? 'piattaforma live' : 'live platform'} · brumpatenti.it
→ {lang === 'it' ? 'screenshot dell\'app in arrivo' : 'app screenshots coming soon'}
)}
); })}
{/* Altri progetti recenti */} {c.cases.some(cs => cs.minor) && (
{lang === 'it' ? 'Altri progetti recenti —' : 'Other recent projects —'} {c.cases.filter(cs => cs.minor).map((cs, i) => ( {cs.client} ({cs.title.replace(/\.$/, '')}) ))}
)}

{/* TESTIMONIALS — nascosti per ora finché non arrivano i testi reali ---- */} {false && (
{c.testimonialsKicker.replace(/^\d+\s*—\s*/, '')}
{c.testimonials.map((t, i) => { const cleaned = t.quote.replace(/^\[|\]$/g, ''); return (
{cleaned}
{t.author} — {t.role}, {t.company} {t.placeholder && (
{lang === 'it' ? 'placeholder — da raccogliere' : 'placeholder — to collect'}
)}
); })}
)} {/* FAQ — disinnesca obiezioni ----------------------------------- */}
{c.faqKicker.replace(/^\d+\s*—\s*/, '')}

{c.faqIntro}

{c.faqs.map((f, i) => { const isOpen = openFaq === i; return (
setOpenFaq(isOpen ? -1 : i)} style={{ borderTop: `1px solid ${faint}`, borderBottom: i === c.faqs.length - 1 ? `1px solid ${faint}` : 'none', padding: '28px 0', cursor: 'none', transition: 'padding-left .35s cubic-bezier(.2,.7,.3,1)', }} onMouseEnter={(e) => { e.currentTarget.style.paddingLeft = '16px'; }} onMouseLeave={(e) => { e.currentTarget.style.paddingLeft = '0px'; }}>
{String(i + 1).padStart(2, '0')}

{f.q}

+

{f.a}

); })}

{c.footerAvailability}


{/* CTA --------------------------------------------------------- */}
{c.ctaKicker.replace(/^\d+\s*—\s*/, '')}

{lang === 'it' ? ( <>Scrivimi cosa stai costruendo. ) : ( <>Tell me what you're building. )}

{c.ctaBody}

{c.ctaButton}
{c.ctaAlt} Calendly
→ {c.ctaSub}

{/* FOOTER */} {!isTouch && }
); } window.TypeCleanArtboard = TypeCleanArtboard;