// Shared components for WordyTrainer landing
// Provides: WordyLogo, PopupCard, DesktopWithPopup, Eyebrow, Section, IconSlot
const { useState, useEffect, useRef } = React;
/* ---------- Logo ---------- */
function WordyMark({ size = 36 }) {
// The W-in-orange-circle mark. Drawn in SVG so it stays crisp on any background.
return (
);
}
function WordyLogo({ size = 32, color = 'var(--ink)', variant = 'brand' }) {
// Brand variant uses the official PNG (W mark + "Wordy the app" wordmark).
// It contains its own colour, so we don't recolour it.
if (variant === 'brand') {
// The PNG is 250×61 — a ratio of ~4.1. We size by height so it lines up
// with adjacent buttons. `size` here means desired height in px.
return (
);
}
// Mark-only fallback (legacy)
return (
WordyTrainer
);
}
// Brand logo helper — wraps the PNG with light/dark-aware backdrop tweaks.
// On dark sections the PNG sits fine (it has its own white-glow circle),
// but text portion ("Wordy the app") is dark — we apply a light/dark
// inversion via filter only when explicitly asked.
function WordyBrandLogo({ height = 32, onDark = false }) {
return (
);
}
/* ---------- Pop-up card (hero metaphor) ---------- */
function PopupCard({ word = 'seat', transcription = '[siːt]', translation = 'сиденье', meta, onDismiss, compact = false }) {
return (
{word}
{transcription}
{translation}
×
{meta && (
{meta}
)}
);
}
/* ---------- Desktop frame with pop-up overlaid (hero visual) ---------- */
function DesktopWithPopup({ screenshot, popup, popupPos = 'br' }) {
// popupPos: 'br' | 'bl' | 'tr' | 'tl'
const posMap = {
br: { right: '6%', bottom: '10%' },
bl: { left: '6%', bottom: '10%' },
tr: { right: '6%', top: '14%' },
tl: { left: '6%', top: '14%' },
};
return (
WordyTrainer
{screenshot && (
)}
{popup && (
{popup}
)}
);
}
/* ---------- Section chrome ---------- */
function Eyebrow({ children, color }) {
return {children}
;
}
function Section({ children, id, screenLabel, bg, style, pad = true }) {
return (
);
}
/* ---------- Simple icon slots (line style) ---------- */
function LineIcon({ name, size = 22, color = 'currentColor' }) {
const stroke = { stroke: color, strokeWidth: 1.6, fill: 'none', strokeLinecap: 'round', strokeLinejoin: 'round' };
const map = {
sparkles: ,
bell: ,
upload: ,
globe: ,
chat: ,
wand: ,
clock: ,
repeat: ,
check: ,
arrowRight: ,
};
return {map[name] || map.check} ;
}
/* ---------- Animated rotating popup (used in hero) ---------- */
function RotatingPopup({ words, interval = 3400, compact }) {
const [i, setI] = useState(0);
useEffect(() => {
const t = setInterval(() => setI((x) => (x + 1) % words.length), interval);
return () => clearInterval(t);
}, [words.length, interval]);
const w = words[i];
return (
);
}
/* ---------- Language dropdown ---------- */
// Single shared dropdown reused by all four landings.
// `theme` controls colours so it sits naturally on light / dark / purple / terminal navs.
// Themes: 'light' (default), 'dark', 'purple', 'terminal'.
function LangDropdown({ lang, onLang, theme = 'light' }) {
const [open, setOpen] = useState(false);
const ref = useRef(null);
const langs = window.WORDY_LANGS || [{ code: 'en', label: 'English', native: 'English', flag: '🇬🇧' }];
const current = langs.find(l => l.code === lang) || langs[0];
useEffect(() => {
if (!open) return;
const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
document.addEventListener('mousedown', onDoc);
document.addEventListener('keydown', onKey);
return () => { document.removeEventListener('mousedown', onDoc); document.removeEventListener('keydown', onKey); };
}, [open]);
// Theme tokens
const T = {
light: {
bg: 'var(--bg-soft)', border: '1px solid var(--line)', color: 'var(--ink)',
menuBg: '#fff', menuBorder: '1px solid var(--line)', menuShadow: '0 12px 32px rgba(0,0,0,0.10)',
hover: 'var(--bg-soft)', activeBg: 'var(--ink)', activeColor: '#fff', radius: 999, mradius: 12,
},
dark: {
bg: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.12)', color: '#fff',
menuBg: '#1B1A20', menuBorder: '1px solid rgba(255,255,255,0.10)', menuShadow: '0 12px 32px rgba(0,0,0,0.45)',
hover: 'rgba(255,255,255,0.06)', activeBg: '#fff', activeColor: 'var(--ink)', radius: 999, mradius: 12,
},
purple: {
bg: '#F5F2FF', border: '1px solid #E5DDFB', color: '#5B21B6',
menuBg: '#fff', menuBorder: '1px solid #E5DDFB', menuShadow: '0 12px 32px rgba(91,33,182,0.14)',
hover: '#F5F2FF', activeBg: '#7C3AED', activeColor: '#fff', radius: 999, mradius: 12,
},
terminal: {
bg: 'transparent', border: '1.5px solid #1A1814', color: '#1A1814',
menuBg: '#FAF6EC', menuBorder: '1.5px solid #1A1814', menuShadow: '4px 4px 0 0 #1A1814',
hover: '#F0EAD8', activeBg: '#1A1814', activeColor: '#FAF6EC', radius: 2, mradius: 2,
},
}[theme] || {};
const isMono = theme === 'terminal';
const fontFam = isMono ? 'var(--font-mono)' : 'var(--font-mono)';
return (
setOpen(o => !o)}
aria-haspopup="listbox"
aria-expanded={open}
style={{
display: 'inline-flex', alignItems: 'center', gap: 8,
padding: '6px 10px 6px 12px',
background: T.bg, border: T.border, color: T.color,
borderRadius: T.radius,
fontSize: 12, fontWeight: 600, fontFamily: fontFam,
letterSpacing: '0.06em', cursor: 'pointer',
lineHeight: 1.2,
}}
>
{current.code.toUpperCase()}
{open && (
{langs.map(l => {
const active = l.code === lang;
return (
{ onLang(l.code); setOpen(false); }}
style={{
width: '100%', textAlign: 'left',
display: 'flex', alignItems: 'center', gap: 10,
padding: '8px 10px',
background: active ? T.activeBg : 'transparent',
color: active ? T.activeColor : T.color,
border: 'none', borderRadius: isMono ? 0 : 8,
fontSize: 13, fontFamily: 'var(--font-sans)', fontWeight: active ? 600 : 500,
cursor: 'pointer',
}}
onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = T.hover; }}
onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = 'transparent'; }}
>
{l.flag}
{l.native}
{l.code.toUpperCase()}
);
})}
)}
);
}
/* ---------- Export to global scope for other scripts ---------- */
Object.assign(window, {
WordyMark, WordyLogo, WordyBrandLogo, PopupCard, DesktopWithPopup, Eyebrow, Section, LineIcon, RotatingPopup, LangDropdown,
});