/* global React, Icons, AI, AutoText, Field, Section, PROFILE_RESUMES, PROFILE_LETTERS, COVER_TEMPLATES, getProfile, ExportButton, PdfButton, Pro */ // ============================================================ // Cover letters — renderer (CoverPaper) + editor (CoverScreen) // A cover-letter template is a layout descriptor in profiles.jsx // (COVER_TEMPLATES). The same accent system as résumés drives color // via --pa / --pa2. Content lives in a "cover doc" (see makeCoverDoc). // ============================================================ const { useState: useStateCL, useEffect: useEffectCL, useRef: useRefCL } = React; const initialsCL = (name) => (name || "?").trim().split(" ").map((w) => w[0]).slice(0, 2).join("").toUpperCase(); // build a default cover doc from a profile's résumé + draft copy function makeCoverDoc(profileId) { const r = PROFILE_RESUMES[profileId] || {}; const l = PROFILE_LETTERS[profileId] || {}; return { profileId, name: r.name || "Your Name", title: r.title || "", email: r.email || "", phone: r.phone || "", location: r.location || "", website: r.website || "", date: "June 3, 2026", recipientName: l.manager || "Hiring Manager", recipientTitle: l.managerTitle || "", company: l.company || "Company", role: l.role || r.title || "the role", greeting: l.manager ? `Dear ${l.manager},` : "Dear Hiring Manager,", paras: (l.paras && l.paras.length) ? [...l.paras] : [""], closing: "Sincerely,", signoff: r.name || "Your Name", }; } // ---- sender identity block (varies subtly by layout) ---- function CoverContacts({ d }) { const items = [ ["mail", "email", d.email], ["phone", "phone", d.phone], ["pin", "location", d.location], ["globe", "website", d.website], ].filter(([, , v]) => v); return (
{items.map(([ic, field, v], i) => { const Ic = Icons[ic] || Icons.tag; return {v}; })}
); } function CoverHeader({ d, layout }) { const parts = (d.name || "").split(" "); if (layout === "bold") { return (

{d.name}

{d.title &&
{d.title}
}
); } if (layout === "executive") { return (
{initialsCL(d.name)}

{d.name}

{d.title &&
{d.title}
}
); } if (layout === "creative") { return (

{parts[0]} {parts.length > 1 && {parts.slice(1).join(" ")}}

{d.title &&
{d.title}
}
); } if (layout === "minimal") { return (

{d.name}

); } if (layout === "dark") { return (

{d.name}

{d.title &&
{d.title}
}
); } if (layout === "mono") { return (
// cover-letter.txt

{d.name}

{d.title &&
{d.title}
}
); } // classic (centered) return (

{d.name}

{d.title &&
{d.title}
}
); } function CoverBody({ d }) { return (
{d.date}
{d.recipientName && {d.recipientName}} {d.recipientTitle && {d.recipientTitle}} {d.company && {d.company}}
{d.role &&
Re: {d.role}
}

{d.greeting}

{(d.paras || []).map((p, i) => (p && p.trim() ?

{p}

: null))}

{d.closing}

{d.signoff}

); } function CoverPaper({ doc, templateId, style, accent, accent2 }) { const base = style || (COVER_TEMPLATES.find((t) => t.id === templateId)) || COVER_TEMPLATES[0]; const layout = base.layout; const sv = { "--pa": accent || base.accent, "--pa2": accent2 || base.accent2 }; const sidebar = layout === "modern"; const serifCls = base.serif === true ? "cover-serif" : base.serif === false ? "cover-sans" : ""; return (
{sidebar ? (
) : ( <> )}
); } // Derive a cover-letter style that mirrors a résumé template's design language // (header archetype, serif/sans voice, accent colors) so the appended cover // page reads as page 3 of the same document — not a separate design. function coverStyleForTemplate(t, accent, accent2) { if (!t) return { id: "auto", layout: "classic", serif: true, accent, accent2 }; const head = t.head || ""; const serifHeads = new Set(["ibank", "serifCenter", "exec", "cv", "legal", "editorial", "formal", "ruled"]); const bandHeads = new Set(["band", "banner", "vivid", "techband", "warmband", "card"]); const classicHeads = new Set(["ibank", "serifCenter", "cv", "legal", "formal", "centered", "ruled", "clinical", "ruledpro", "timeline"]); const minimalHeads = new Set(["ats", "swiss", "labeled", "designer"]); let layout; if (head === "dark" || t.id === "onyx") layout = "dark"; else if (head === "codecard" || t.id === "terminal") layout = "mono"; else if (t.arch === "sidebar") layout = "modern"; else if (head === "exec") layout = "executive"; else if (bandHeads.has(head)) layout = "bold"; else if (head === "editorial") layout = "creative"; else if (classicHeads.has(head)) layout = "classic"; else if (minimalHeads.has(head)) layout = "minimal"; else layout = serifHeads.has(head) ? "classic" : "minimal"; const serif = serifHeads.has(head) || layout === "classic" || layout === "executive"; return { id: "auto-" + t.id, layout, serif: layout === "mono" ? undefined : serif, accent: accent || t.accent, accent2: accent2 || t.accent2, }; } // scale a 768px cover paper to fit a container (gallery thumbs) function CoverFit({ doc, templateId, designWidth = 768 }) { const ref = useRefCL(null); const [scale, setScale] = useStateCL(0.4); useEffectCL(() => { const el = ref.current; if (!el) return; const fit = () => setScale((el.clientWidth || designWidth * 0.4) / designWidth); fit(); const ro = new ResizeObserver(fit); ro.observe(el); return () => ro.disconnect(); }, [designWidth]); return (
); } // ============================================================ // Cover-letter editor screen // ============================================================ // which form section controls each editable field on the letter const COVER_FIELD_SEC = { name: "details", title: "details", email: "details", phone: "details", location: "details", website: "details", date: "role", recipientName: "role", recipientTitle: "role", company: "role", role: "role", greeting: "letter", closing: "letter", signoff: "letter", }; function CoverScreen({ doc, setDoc, templateId, setTemplateId, accent, setAccent, onBack }) { const tpl = COVER_TEMPLATES.find((t) => t.id === templateId) || COVER_TEMPLATES[0]; const [busy, setBusy] = useStateCL(false); const [toast, setToast] = useStateCL(""); const [zoom, setZoom] = useStateCL(0.8); const [paperH, setPaperH] = useStateCL(900); const stageRef = useRefCL(null); const innerRef = useRefCL(null); const editingRef = useRefCL(false); // true while an inline edit is focused const flash = (m) => { setToast(m); setTimeout(() => setToast(""), 2600); }; const pa = accent.custom ? accent : { a: tpl.accent, b: tpl.accent2 }; // ---- inline editing on the letter + click-to-locate the form section ---- const commitCover = (path, val) => { setDoc((x) => { const d = { ...x }; if (path.startsWith("para:")) { const i = Number(path.split(":")[1]); d.paras = (d.paras || []).map((p, j) => (j === i ? val : p)); } else { d[path] = val; } return d; }); }; const onCoverMouseDown = (e) => { if (!e.target.closest(".cover")) return; const ed = e.target.closest("[data-edit]"); const path = ed ? ed.getAttribute("data-edit") : null; if (path) { const sec = path.startsWith("para") ? "letter" : COVER_FIELD_SEC[path]; if (sec) window.dispatchEvent(new CustomEvent("rb-focus-section", { detail: { key: sec } })); } if (!ed || ed.getAttribute("contenteditable") === "true") return; const orig = ed.textContent; const multi = path === "greeting" || path.startsWith("para"); ed.setAttribute("contenteditable", "true"); ed.classList.add("rb-edit-on"); editingRef.current = true; const finish = () => { ed.removeEventListener("blur", finish); ed.removeEventListener("keydown", onKey); ed.removeAttribute("contenteditable"); ed.classList.remove("rb-edit-on"); editingRef.current = false; const val = ed.textContent.replace(/\u00a0/g, " ").replace(/[ \t]+/g, " ").trim(); if (val !== orig.trim()) commitCover(path, val); }; const onKey = (ev) => { if (ev.key === "Enter" && !(multi && ev.shiftKey)) { ev.preventDefault(); ed.blur(); } else if (ev.key === "Escape") { ev.preventDefault(); ed.textContent = orig; ed.blur(); } }; ed.addEventListener("blur", finish); ed.addEventListener("keydown", onKey); }; useEffectCL(() => { const fit = () => { const stage = stageRef.current; if (!stage) return; const avail = stage.clientWidth - 56; setZoom(Math.min(0.95, Math.max(0.3, avail / 768))); }; fit(); const ro = new ResizeObserver(fit); if (stageRef.current) ro.observe(stageRef.current); return () => ro.disconnect(); }, []); useEffectCL(() => { const el = innerRef.current; if (!el) return; const measure = () => { if (editingRef.current) return; setPaperH(el.offsetHeight || 900); }; measure(); const ro = new ResizeObserver(measure); ro.observe(el); return () => ro.disconnect(); }, [templateId, doc]); const set = (patch) => setDoc((x) => ({ ...x, ...patch })); const setPara = (i, v) => set({ paras: doc.paras.map((p, x) => x === i ? v : p) }); const addPara = () => set({ paras: [...doc.paras, ""] }); const delPara = (i) => set({ paras: doc.paras.filter((_, x) => x !== i) }); const aiWrite = async () => { if (!Pro.guardAI()) return; setBusy(true); const r = PROFILE_RESUMES[doc.profileId] || {}; const exp = (r.experience || [])[0] || {}; const prompt = `Write a professional cover letter body of exactly 3 paragraphs for a job application. Applicant: ${doc.name}, ${doc.title}. Applying for: ${doc.role} at ${doc.company}. Key achievements to weave in: ${(exp.bullets || []).slice(0, 3).join(" ")} Top skills: ${(r.skills || []).map((s) => (s && s.name) || s).slice(0, 6).join(", ")}. Tone: confident, specific, not generic. Include real metrics from the achievements. Return ONLY the 3 paragraphs separated by a blank line — no greeting, no sign-off, no preamble.`; const out = await AI.run(prompt); setBusy(false); if (out) { const paras = out.split(/\n\s*\n/).map((s) => s.trim()).filter(Boolean); set({ paras: paras.length ? paras : doc.paras }); Pro.recordAI(); flash("Cover letter drafted with AI"); } else flash("Couldn't reach AI — try again"); }; const accentOptions = [ { a: "#1f3a5f", b: "#5a7a9c" }, { a: "#4f46e5", b: "#06b6d4" }, { a: "#ef5b2b", b: "#ffb020" }, { a: "#7c3aed", b: "#06b6d4" }, { a: "#111111", b: "#6b6b6b" }, { a: "#117a8b", b: "#3aa7a0" }, ]; return (
{accentOptions.map((o, i) => (
innerRef.current} fileName={`${(doc.name || "Cover Letter").trim()} — Cover Letter`} reason="cover" />

Write your cover letter

Pre-filled to match your profile. Click any line on the letter to edit it in place — clicking also jumps to its controls here. Or hit to redraft the body with AI.

set({ role: v })} /> set({ company: v })} />
set({ recipientName: v, greeting: `Dear ${v},` })} /> set({ recipientTitle: v })} />
set({ date: v })} />
set({ name: v, signoff: v })} /> set({ title: v })} />
set({ email: v })} /> set({ phone: v })} />
set({ location: v })} /> set({ website: v })} />
set({ greeting: v })} /> Body paragraphs
{doc.paras.map((p, i) => (
setPara(i, v)} placeholder="Write a paragraph…" />
))}
set({ closing: v })} /> set({ signoff: v })} />
Keep it to one page — you're set.
{tpl.name} Click any text to edit {Math.round(zoom * 100)}%
{toast &&
{toast}
}
); } Object.assign(window, { CoverPaper, CoverFit, CoverScreen, makeCoverDoc, coverStyleForTemplate });