// components-hero.jsx — Single-focus hero. Left: copy + CTAs. Right: hero image slot. // No more decorative quill SVG; the typewriter mock lives as a fallback inside the slot. function useTypewriter(paragraphs, opts = {}) { const { charDelay = 80, paraGap = 1400, restart = 0 } = opts; const [text, setText] = React.useState(""); const [paraIdx, setParaIdx] = React.useState(0); React.useEffect(() => { setText(""); setParaIdx(0); }, [restart]); React.useEffect(() => { if (!paragraphs.length) return; let active = true; let i = 0; const para = paragraphs[paraIdx % paragraphs.length]; function typeNext() { if (!active) return; if (i <= para.length) { setText(para.slice(0, i)); i++; const jitter = (Math.random() * 60) - 20; const last = para[i - 1]; const punctPause = /[,。、;:!??!,.;:]/.test(last) ? 240 : 0; setTimeout(typeNext, charDelay + jitter + punctPause); } else { setTimeout(() => { if (!active) return; setParaIdx((p) => p + 1); }, paraGap); } } const start = setTimeout(typeNext, 500); return () => { active = false; clearTimeout(start); }; }, [paraIdx, paragraphs, charDelay, paraGap]); return text; } function DownloadSplit({ copy }) { const [open, setOpen] = React.useState(false); const ref = React.useRef(null); React.useEffect(() => { function onDoc(e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false); } document.addEventListener("mousedown", onDoc); return () => document.removeEventListener("mousedown", onDoc); }, []); return (
{copy.hero.dlMac}·dmg {copy.hero.dlMacIntel}·dmg {copy.hero.dlWin}·exe {copy.hero.dlWeb}·web
); } // Typewriter mock — shown as a styled fallback INSIDE the hero PicSlot // until the user drops in a real screenshot at uploads/hero.webp. function HeroProseFallback({ copy, lang }) { const txt = useTypewriter(copy.typewriter.paragraphs, { charDelay: lang === "zh" ? 95 : 35, paraGap: 1600, restart: lang, }); return (
{lang === "zh" ? "雨中客 / 卷一 / 第 1 章 · 破庙夜雨" : "Rainy Guest / Book I / Ch. 1 · Rain at the Temple"}

{copy.typewriter.title}

{txt}

{lang === "zh" ? "第 1 章 · 破庙夜雨" : "Ch. 1 · Rain at the Temple"} · {lang === "zh" ? "3,182 字" : "3,182 words"} · {lang === "zh" ? "已保存 09:42" : "Saved 09:42"} · {lang === "zh" ? "本地" : "Local"}
); } function Hero({ copy, lang }) { const heroSrc = "uploads/hero.png"; return (

{copy.hero.titleA} {copy.hero.titleB}

{copy.hero.sub}

); } // Tries to load the hero image; falls back to the prose-mockup component above. // Wrapped in a card so the fallback feels intentional, not broken. function HeroShot({ src, copy, lang }) { const [errored, setErrored] = React.useState(false); const [loaded, setLoaded] = React.useState(false); const showImg = src && window.UPLOADED_IMAGES && window.UPLOADED_IMAGES.has(src) && !errored; return (
{showImg && ( {lang setLoaded(true)} onError={() => setErrored(true)} /> )} {(!showImg || !loaded) && (
HERO · {src} {lang === "zh" ? "上传一张真实写作界面截图替换此区域" : "Drop a real product screenshot here"}
)}
); } Object.assign(window, { Hero, HeroShot, HeroProseFallback, DownloadSplit, useTypewriter });