Redesign Lebenslauf: monochrome single-column HR layout
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+52
-36
@@ -1,7 +1,8 @@
|
|||||||
// Document generation.
|
// Document generation.
|
||||||
//
|
//
|
||||||
// Architecture: the *design* lives here as a fixed, high-quality two-column
|
// Architecture: the *design* lives here as a fixed, high-quality template
|
||||||
// template (dark sidebar + white main column). The LLM only supplies the TEXT
|
// (clean monochrome single-column layout for both cover letter and résumé,
|
||||||
|
// reverse-chronological, DIN-ish margins). The LLM only supplies the TEXT
|
||||||
// content (tailored to the job) as structured data — it never touches layout.
|
// content (tailored to the job) as structured data — it never touches layout.
|
||||||
// The text stays grounded in the user's own base documents; nothing is invented.
|
// The text stays grounded in the user's own base documents; nothing is invented.
|
||||||
|
|
||||||
@@ -477,10 +478,10 @@ function rule(doc, S, dry, y, x1, x2, color, weight) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Resume (Lebenslauf) — clean single-column layout
|
// Resume (Lebenslauf) — monochrome single column, reverse-chronological
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|
||||||
const RV = { mx: 20, top: 16, bottom: 14 };
|
const RV = { mx: 20, top: 18, bottom: 14 };
|
||||||
const RV_W = PAGE.w - RV.mx * 2; // 170 content width
|
const RV_W = PAGE.w - RV.mx * 2; // 170 content width
|
||||||
const RV_R = PAGE.w - RV.mx; // 190 right edge (right-aligned dates)
|
const RV_R = PAGE.w - RV.mx; // 190 right edge (right-aligned dates)
|
||||||
const RV_DATE_W = 34; // reserved right zone for the date column
|
const RV_DATE_W = 34; // reserved right zone for the date column
|
||||||
@@ -493,28 +494,31 @@ const RC = {
|
|||||||
|
|
||||||
// Section heading: bold uppercase tracked label with a thin hairline beneath.
|
// Section heading: bold uppercase tracked label with a thin hairline beneath.
|
||||||
function cvHeading(doc, S, dry, cur, title) {
|
function cvHeading(doc, S, dry, cur, title) {
|
||||||
const pt = 11.5;
|
const pt = 11;
|
||||||
cur.y += 2.5 * S;
|
cur.y += 3 * S;
|
||||||
doc.setFont('helvetica', 'bold');
|
doc.setFont('helvetica', 'bold');
|
||||||
doc.setFontSize(pt * S);
|
doc.setFontSize(pt * S);
|
||||||
if (!dry) {
|
if (!dry) {
|
||||||
doc.setCharSpace(0.4 * S);
|
doc.setCharSpace(0.6 * S);
|
||||||
doc.setTextColor(RC.ink[0], RC.ink[1], RC.ink[2]);
|
doc.setTextColor(RC.ink[0], RC.ink[1], RC.ink[2]);
|
||||||
doc.text(String(title).toUpperCase(), RV.mx, cur.y + pt * S * PT * 0.76);
|
doc.text(String(title).toUpperCase(), RV.mx, cur.y + pt * S * PT * 0.76);
|
||||||
doc.setCharSpace(0);
|
doc.setCharSpace(0);
|
||||||
doc.setDrawColor(RC.hair[0], RC.hair[1], RC.hair[2]);
|
doc.setDrawColor(RC.hair[0], RC.hair[1], RC.hair[2]);
|
||||||
doc.setLineWidth(0.3 * S);
|
doc.setLineWidth(0.3 * S);
|
||||||
doc.line(RV.mx, cur.y + pt * S * PT * 0.98, RV_R, cur.y + pt * S * PT * 0.98);
|
doc.line(RV.mx, cur.y + pt * S * PT * 1.0, RV_R, cur.y + pt * S * PT * 1.0);
|
||||||
}
|
}
|
||||||
cur.y += pt * S * PT + 3.2 * S;
|
cur.y += pt * S * PT + 3.4 * S;
|
||||||
}
|
}
|
||||||
|
|
||||||
// One row: bold title (left) + muted date (right-aligned) on a single baseline.
|
// One row: title (left) + muted date (right-aligned) on a single baseline.
|
||||||
function cvRow(doc, S, dry, cur, leftText, rightText, leftPt) {
|
// `leftStyle` controls the title weight ('bold' for experience/education,
|
||||||
|
// 'normal' for the language list).
|
||||||
|
function cvRow(doc, S, dry, cur, leftText, rightText, leftPt, leftStyle) {
|
||||||
leftPt = leftPt || 11;
|
leftPt = leftPt || 11;
|
||||||
|
leftStyle = leftStyle || 'bold';
|
||||||
const rightPt = 9.5;
|
const rightPt = 9.5;
|
||||||
const leftW = RV_W - RV_DATE_W;
|
const leftW = RV_W - RV_DATE_W;
|
||||||
doc.setFont('helvetica', 'bold');
|
doc.setFont('helvetica', leftStyle);
|
||||||
doc.setFontSize(leftPt * S);
|
doc.setFontSize(leftPt * S);
|
||||||
const lines = doc.splitTextToSize(String(leftText || ''), leftW);
|
const lines = doc.splitTextToSize(String(leftText || ''), leftW);
|
||||||
const lineH = leftPt * S * PT * 1.18;
|
const lineH = leftPt * S * PT * 1.18;
|
||||||
@@ -546,36 +550,51 @@ function cvBullets(doc, S, dry, cur, items, x, w, pt = 9.3) {
|
|||||||
}
|
}
|
||||||
cur.y += pt * S * PT * 1.3;
|
cur.y += pt * S * PT * 1.3;
|
||||||
});
|
});
|
||||||
cur.y += 0.8 * S;
|
cur.y += 0.9 * S;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Two-column bullet grid — used for the skills list so a dozen short items
|
||||||
|
// stay compact and scannable instead of one long vertical column.
|
||||||
|
function cvTwoColBullets(doc, S, dry, cur, items, x, w, pt = 9.3) {
|
||||||
|
const gap = 8 * S;
|
||||||
|
const colW = (w - gap) / 2;
|
||||||
|
const half = Math.ceil(items.length / 2);
|
||||||
|
const lc = { y: cur.y }, rc = { y: cur.y };
|
||||||
|
cvBullets(doc, S, dry, lc, items.slice(0, half), x, colW, pt);
|
||||||
|
cvBullets(doc, S, dry, rc, items.slice(half), x + colW + gap, colW, pt);
|
||||||
|
cur.y = Math.max(lc.y, rc.y);
|
||||||
|
}
|
||||||
|
|
||||||
function cvExperience(doc, S, dry, cur, e) {
|
function cvExperience(doc, S, dry, cur, e) {
|
||||||
cvRow(doc, S, dry, cur, e.titel, e.zeitraum, 11);
|
cvRow(doc, S, dry, cur, e.titel, e.zeitraum, 11, 'bold');
|
||||||
if (e.firma) write(doc, S, dry, cur, e.firma, { pt: 9.7, color: RC.sub, x: RV.mx, width: RV_W, factor: 1.18 });
|
if (e.firma) write(doc, S, dry, cur, e.firma, { pt: 9.7, color: RC.sub, x: RV.mx, width: RV_W, factor: 1.18 });
|
||||||
if (e.punkte && e.punkte.length) { cur.y += 0.8 * S; cvBullets(doc, S, dry, cur, e.punkte, RV.mx, RV_W); }
|
if (e.punkte && e.punkte.length) { cur.y += 1 * S; cvBullets(doc, S, dry, cur, e.punkte, RV.mx, RV_W); }
|
||||||
cur.y += 2.6 * S;
|
cur.y += 3 * S;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cvEducation(doc, S, dry, cur, e) {
|
function cvEducation(doc, S, dry, cur, e) {
|
||||||
cvRow(doc, S, dry, cur, e.abschluss, e.zeitraum, 11);
|
cvRow(doc, S, dry, cur, e.abschluss, e.zeitraum, 11, 'bold');
|
||||||
if (e.institution) write(doc, S, dry, cur, e.institution, { pt: 9.7, color: RC.sub, x: RV.mx, width: RV_W, factor: 1.18 });
|
if (e.institution) write(doc, S, dry, cur, e.institution, { pt: 9.7, color: RC.sub, x: RV.mx, width: RV_W, factor: 1.18 });
|
||||||
if (e.zusatz) write(doc, S, dry, cur, e.zusatz, { pt: 9.5, color: RC.sub, x: RV.mx, width: RV_W, factor: 1.2 });
|
if (e.zusatz) write(doc, S, dry, cur, e.zusatz, { pt: 9.5, color: RC.sub, x: RV.mx, width: RV_W, factor: 1.2 });
|
||||||
cur.y += 2.4 * S;
|
cur.y += 2.6 * S;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header: name, position subtitle, single muted contact line, thin rule.
|
// Header: name + position (top-left) and a right-aligned contact block
|
||||||
|
// (top-right), separated by a confident rule — the canonical modern CV head.
|
||||||
function composeCvHeader(doc, S, dry, cur, header, titel) {
|
function composeCvHeader(doc, S, dry, cur, header, titel) {
|
||||||
write(doc, S, dry, cur, header.name, { pt: 20, style: 'bold', color: RC.ink, x: RV.mx, width: RV_W, factor: 1.1 });
|
const top = cur.y;
|
||||||
if (titel) write(doc, S, dry, cur, titel, { pt: 11, color: RC.sub, x: RV.mx, width: RV_W, factor: 1.2 });
|
const leftW = RV_W * 0.62;
|
||||||
const contact = [header.email, header.telefon, cityName(header) || header.ort, header.webseite]
|
write(doc, S, dry, cur, header.name, { pt: 22, style: 'bold', color: RC.ink, x: RV.mx, width: leftW, factor: 1.05 });
|
||||||
.filter(Boolean).join(' · ');
|
if (titel) write(doc, S, dry, cur, titel, { pt: 12, color: RC.sub, x: RV.mx, width: leftW, factor: 1.15 });
|
||||||
if (contact) {
|
const leftBottom = cur.y;
|
||||||
cur.y += 1.4 * S;
|
|
||||||
write(doc, S, dry, cur, contact, { pt: 9, color: RC.sub, x: RV.mx, width: RV_W, factor: 1.2 });
|
const contactLines = [header.email, header.telefon, cityName(header) || header.ort, header.webseite].filter(Boolean);
|
||||||
}
|
const rc = { y: top };
|
||||||
cur.y += 3 * S;
|
contactLines.forEach((line) => write(doc, S, dry, rc, line, { pt: 9, color: RC.sub, x: RV.mx, width: RV_W, align: 'right', right: RV_R, factor: 1.34 }));
|
||||||
rule(doc, S, dry, cur.y, RV.mx, RV_R, RC.ink, 0.5);
|
|
||||||
|
cur.y = Math.max(leftBottom, rc.y) + 3.2 * S;
|
||||||
|
rule(doc, S, dry, cur.y, RV.mx, RV_R, RC.ink, 0.6);
|
||||||
cur.y += 5 * S;
|
cur.y += 5 * S;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -583,7 +602,7 @@ function composeCV(doc, S, dry, cur, { cv, header, titel }) {
|
|||||||
composeCvHeader(doc, S, dry, cur, header, titel);
|
composeCvHeader(doc, S, dry, cur, header, titel);
|
||||||
if (cv.profil) {
|
if (cv.profil) {
|
||||||
cvHeading(doc, S, dry, cur, 'Profil');
|
cvHeading(doc, S, dry, cur, 'Profil');
|
||||||
write(doc, S, dry, cur, cv.profil, { pt: 9.7, color: RC.ink, x: RV.mx, width: RV_W, factor: 1.42 });
|
write(doc, S, dry, cur, cv.profil, { pt: 10, color: RC.ink, x: RV.mx, width: RV_W, factor: 1.45 });
|
||||||
}
|
}
|
||||||
if (cv.berufserfahrung.length) {
|
if (cv.berufserfahrung.length) {
|
||||||
cvHeading(doc, S, dry, cur, 'Berufserfahrung');
|
cvHeading(doc, S, dry, cur, 'Berufserfahrung');
|
||||||
@@ -596,18 +615,15 @@ function composeCV(doc, S, dry, cur, { cv, header, titel }) {
|
|||||||
});
|
});
|
||||||
if (cv.kenntnisse.length) {
|
if (cv.kenntnisse.length) {
|
||||||
cvHeading(doc, S, dry, cur, 'Fähigkeiten');
|
cvHeading(doc, S, dry, cur, 'Fähigkeiten');
|
||||||
cvBullets(doc, S, dry, cur, cv.kenntnisse, RV.mx, RV_W);
|
cvTwoColBullets(doc, S, dry, cur, cv.kenntnisse, RV.mx, RV_W);
|
||||||
}
|
}
|
||||||
if (cv.sprachen.length) {
|
if (cv.sprachen.length) {
|
||||||
cvHeading(doc, S, dry, cur, 'Sprachen');
|
cvHeading(doc, S, dry, cur, 'Sprachen');
|
||||||
cv.sprachen.forEach((s) => {
|
cv.sprachen.forEach((s) => cvRow(doc, S, dry, cur, s.sprache, s.niveau, 9.7, 'normal'));
|
||||||
const t = s.niveau ? `${s.sprache} – ${s.niveau}` : s.sprache;
|
|
||||||
write(doc, S, dry, cur, t, { pt: 9.3, color: RC.ink, x: RV.mx, width: RV_W, factor: 1.3 });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (cv.hobbys.length) {
|
if (cv.hobbys.length) {
|
||||||
cvHeading(doc, S, dry, cur, 'Interessen');
|
cvHeading(doc, S, dry, cur, 'Interessen');
|
||||||
cvBullets(doc, S, dry, cur, cv.hobbys, RV.mx, RV_W);
|
write(doc, S, dry, cur, cv.hobbys.join(', '), { pt: 9.7, color: RC.ink, x: RV.mx, width: RV_W, factor: 1.3 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user