// VibeAlchemy Marketing · shared components
const { useState, useEffect, useRef } = React;
// ── Navigation helpers ────────────────────────────────────────────────────
function go(route, e) {
if (e) e.preventDefault();
window.location.hash = route;
window.scrollTo({ top: 0, behavior: 'instant' });
}
function useRoute() {
const [route, setRoute] = useState(() => window.location.hash.replace(/^#/, '') || '/');
useEffect(() => {
const onHash = () => setRoute(window.location.hash.replace(/^#/, '') || '/');
window.addEventListener('hashchange', onHash);
return () => window.removeEventListener('hashchange', onHash);
}, []);
return route;
}
// ── Brand: Forged V mark + wordmark ───────────────────────────────────────
// CANONICAL SOURCE: ui/src/components/brand/Logo.jsx. This is a hand-kept
// mirror because the marketing pages run via in-browser Babel (not the Vite
// module graph) and cannot import from src/. If the mark geometry or wordmark
// changes, update BOTH this block and Logo.jsx.
// Raw half + seam use theme vars so the mark flips on light surfaces.
let _vaMarkUid = 0;
function LogoMark({ size = 22 }) {
const gid = `va-mk-${++_vaMarkUid}`;
return (
);
}
function BrandWord() {
return (
Vibe Alchemy
);
}
// ── Top nav ───────────────────────────────────────────────────────────────
function TopNav({ route, theme, onToggleTheme }) {
const active = (r) => route === r || (r !== '/' && route.startsWith(r));
const nextTheme = theme === 'dark' ? 'light' : 'dark';
const [mobileOpen, setMobileOpen] = useState(false);
const close = () => setMobileOpen(false);
const navLink = (path, label) => (
{ close(); go(path, e); }}>{label}
);
return (
{ close(); go('/', e); }}>
{navLink('/how-it-works', 'How it works')}
{navLink('/examples', 'Examples')}
{navLink('/pricing', 'Pricing')}
{navLink('/faq', 'FAQ')}
{navLink('/about', 'About')}
);
}
// ── Footer ───────────────────────────────────────────────────────────────
function Footer() {
return (
go('/', e)}>
The validation engine that tells you the truth. Twelve perspectives, three skeptics, one Decision Memo you can defend.
);
}
// ── Section wrappers ─────────────────────────────────────────────────────
function Section({ eyebrow, headline, sub, children, bg, narrow, id, className = '' }) {
return (
{(eyebrow || headline || sub) && (
{eyebrow &&
{eyebrow}
}
{headline &&
{headline} }
{sub &&
{sub}
}
)}
{children}
);
}
// ── Decision Memo artifact (hero visual) ─────────────────────────────────
function MemoArtifact({ verdict = 'cond', verdictLabel = 'Conditional Go', score = 73, title, insight, assumption, redline, meta }) {
return (
Real run · Anonymized
{title || 'Decision Memo'}
{meta || 'D2C Subscription · 2026-05-22 · Run #017'}
{verdictLabel}
Composite Score
{score}
/ 100
Confidence
Moderate · 3 / 5
Key Insight
"{insight || 'Premium home coffee enthusiasts seek diverse, fresh, single-origin beans from specific regions — a $20B market currently underserved by subscriptions that lack true curation and independent roaster focus.'}"
Riskiest Assumption
{assumption || 'Home coffee enthusiasts will readily switch from their existing "meticulously curated system" to a new subscription service for convenience.'}
{redline || 'Test before scaling · 15–20 enthusiast interviews · <20% switch willingness kills it'}
);
}
// ── Doc preview (deliverable tile artifact) ───────────────────────────────
function DocPreview({ kind, pill, pillText }) {
if (kind === 'memo') {
return (
);
}
if (kind === 'market') {
return (
);
}
if (kind === 'red') {
return (
);
}
if (kind === 'pivot') {
return (
);
}
return null;
}
// ── Pipeline stage icons (line-art) ───────────────────────────────────────
const StageIcons = {
Frame: (
),
Research: (
),
Analyse: (
),
Challenge: (
),
Decide: (
),
};
// ── Final CTA block ───────────────────────────────────────────────────────
function FinalCTA({ headline, sub, kicker, source }) {
const isBeta = window.PRIVATE_BETA !== false;
const defaultSub = isBeta
? "Private beta — invitation only. We're inviting founders in small batches. Founding pricing held for invitees."
: 'Free first run. First 100 paying customers lock in 50% off year 1. No card required.';
return (
);
}
// ── Trust strip ───────────────────────────────────────────────────────────
function TrustStrip() {
return (
);
}
// ── FAQ Accordion ─────────────────────────────────────────────────────────
function FAQAccordion({ items }) {
const [open, setOpen] = useState(0);
return (
{items.map((it, i) => (
setOpen(open === i ? -1 : i)}>
{it.q}
+
))}
);
}
// ── Page hero (compact, for inner pages) ──────────────────────────────────
function PageHero({ eyebrow, headline, sub }) {
return (
{eyebrow}
{headline}
{sub &&
{sub}
}
);
}
// ── Waitlist modal (private beta) ─────────────────────────────────────────
// Public API: window.openWaitlist({ source, tier_interest }) dispatches a
// custom event picked up by (mounted once in App).
// `source` should be one of the values accepted by /api/waitlist:
// 'marketing' | 'all_in_page' | 'footer'. `tier_interest` is optional and
// only used as a hint when the user opened the modal from a tier card.
function openWaitlist(detail) {
window.dispatchEvent(new CustomEvent('va:waitlist-open', { detail: detail || {} }));
try { window.track && window.track('waitlist_open', { source: (detail && detail.source) || 'marketing' }); } catch (_) {}
}
function WaitlistRoot() {
const [open, setOpen] = useState(false);
const [meta, setMeta] = useState({ source: 'marketing', tier_interest: null });
const [email, setEmail] = useState('');
const [stage, setStage] = useState('');
const [status, setStatus] = useState('idle'); // idle | submitting | ok | err
const [errMsg, setErrMsg] = useState('');
useEffect(() => {
const onOpen = (e) => {
const d = (e && e.detail) || {};
setMeta({ source: d.source || 'marketing', tier_interest: d.tier_interest || null });
setStatus('idle');
setErrMsg('');
setEmail('');
setStage('');
setOpen(true);
};
window.addEventListener('va:waitlist-open', onOpen);
const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
window.addEventListener('keydown', onKey);
return () => {
window.removeEventListener('va:waitlist-open', onOpen);
window.removeEventListener('keydown', onKey);
};
}, []);
if (!open) return null;
const submit = async (e) => {
e.preventDefault();
if (!email || !/.+@.+\..+/.test(email)) {
setErrMsg('Enter a valid email.');
return;
}
setStatus('submitting');
setErrMsg('');
try {
const res = await fetch('/api/waitlist', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: email.trim(),
tier_interest: meta.tier_interest,
idea_stage: stage || null,
source: meta.source,
}),
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error((body && body.detail && body.detail.message) || 'Couldn\'t reach the waitlist. Try again shortly.');
}
setStatus('ok');
try { window.track && window.track('waitlist_submit', { source: meta.source, tier_interest: meta.tier_interest, idea_stage: stage || null }); } catch (_) {}
} catch (err) {
setStatus('err');
setErrMsg(err.message || 'Something went wrong.');
}
};
return (
{ if (e.target === e.currentTarget) setOpen(false); }}
style={{
position: 'fixed', inset: 0, zIndex: 1000,
background: 'rgba(8, 6, 4, 0.72)', backdropFilter: 'blur(4px)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
padding: 20,
}}
>
setOpen(false)}
style={{
position: 'absolute', top: 12, right: 12, background: 'transparent',
border: 'none', color: 'var(--stone)', fontSize: 18, cursor: 'pointer',
lineHeight: 1, padding: 6,
}}
>✕
{status === 'ok' ? (
YOU'RE IN
Thanks — we'll be in touch.
We're inviting beta users in small batches. You'll hear from us at {email} when your invite is ready. Founding pricing is held for invitees.
setOpen(false)}>Close
) : (
)}
);
}
// Make available globally to other scripts
Object.assign(window, {
go, useRoute, TopNav, Footer, Section, MemoArtifact, DocPreview, StageIcons,
FinalCTA, TrustStrip, FAQAccordion, PageHero,
openWaitlist, WaitlistRoot,
});