/* 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 (
);
}
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))}
);
}
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 ? (
{doc.name}
{doc.title && {doc.title}
}
) : (
<>
>
)}
);
}
// 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 (
Cover letters
setTemplateId(e.target.value)}>
{COVER_TEMPLATES.map((t) => {t.name} )}
{accentOptions.map((o, i) => (
setAccent({ ...o, custom: true })} title="Accent" />
))}
setAccent({ custom: false })}>
{busy ? : }
{busy ? "Writing…" : "Write with AI"}
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…" />
delPara(i)}>
))}
Add 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 });