CV: contact icons, larger meta text, removed heading rules
Draw small vector icons (phone, mail, pin, globe) in front of each contact row instead of a pipe-separated single line; drop the title highlight box and the per-section hair rules for a cleaner header. Bump experience/education meta to 9.5pt ink and titles to 11.5pt bold, and tighten section spacing. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+77
-30
@@ -520,8 +520,7 @@ function cvHeading(doc, S, dry, cur, title, x, w) {
|
|||||||
doc.setCharSpace(0);
|
doc.setCharSpace(0);
|
||||||
}
|
}
|
||||||
cur.y += 11.5 * S * PT + 1.8 * S;
|
cur.y += 11.5 * S * PT + 1.8 * S;
|
||||||
rule(doc, S, dry, cur.y, x, x + w, RC.hair, 0.3);
|
cur.y += 2.4 * S;
|
||||||
cur.y += 3.2 * S;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cvBullets(doc, S, dry, cur, items, x, w, pt = 9.3) {
|
function cvBullets(doc, S, dry, cur, items, x, w, pt = 9.3) {
|
||||||
@@ -533,7 +532,7 @@ function cvBullets(doc, S, dry, cur, items, x, w, pt = 9.3) {
|
|||||||
const lines = doc.splitTextToSize(String(it), tw);
|
const lines = doc.splitTextToSize(String(it), tw);
|
||||||
lines.forEach((ln, i) => {
|
lines.forEach((ln, i) => {
|
||||||
if (!dry) {
|
if (!dry) {
|
||||||
if (i === 0) { doc.setFillColor(RC.sub[0], RC.sub[1], RC.sub[2]); doc.circle(x + 0.8 * S, cur.y + pt * S * PT * 0.42, 0.6 * S, 'F'); }
|
if (i === 0) { doc.setFillColor(RC.ink[0], RC.ink[1], RC.ink[2]); doc.circle(x + 0.8 * S, cur.y + pt * S * PT * 0.42, 0.6 * S, 'F'); }
|
||||||
doc.setTextColor(RC.ink[0], RC.ink[1], RC.ink[2]);
|
doc.setTextColor(RC.ink[0], RC.ink[1], RC.ink[2]);
|
||||||
doc.text(ln, tx, cur.y + pt * S * PT * 0.76);
|
doc.text(ln, tx, cur.y + pt * S * PT * 0.76);
|
||||||
}
|
}
|
||||||
@@ -545,52 +544,102 @@ function cvBullets(doc, S, dry, cur, items, x, w, pt = 9.3) {
|
|||||||
|
|
||||||
function cvExperience(doc, S, dry, cur, e, x, w) {
|
function cvExperience(doc, S, dry, cur, e, x, w) {
|
||||||
const meta = [e.firma, e.zeitraum].filter(Boolean).join(' | ');
|
const meta = [e.firma, e.zeitraum].filter(Boolean).join(' | ');
|
||||||
write(doc, S, dry, cur, meta, { pt: 8.7, color: RC.sub, x, width: w, factor: 1.15 });
|
write(doc, S, dry, cur, meta, { pt: 9.5, color: RC.ink, x, width: w, factor: 1.1 });
|
||||||
write(doc, S, dry, cur, e.titel, { pt: 10.5, style: 'bold', color: RC.ink, x, width: w, factor: 1.2 });
|
write(doc, S, dry, cur, e.titel, { pt: 11.5, style: 'bold', color: RC.ink, x, width: w, factor: 1.15 });
|
||||||
if (e.punkte && e.punkte.length) { cur.y += 0.8 * S; cvBullets(doc, S, dry, cur, e.punkte, x, w); }
|
if (e.punkte && e.punkte.length) { cur.y += 0.6 * S; cvBullets(doc, S, dry, cur, e.punkte, x, w); }
|
||||||
cur.y += 3.4 * S;
|
cur.y += 2.4 * S;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cvEducation(doc, S, dry, cur, e, x, w) {
|
function cvEducation(doc, S, dry, cur, e, x, w) {
|
||||||
const meta = [e.institution, e.zeitraum].filter(Boolean).join(' | ');
|
const meta = [e.institution, e.zeitraum].filter(Boolean).join(' | ');
|
||||||
write(doc, S, dry, cur, meta, { pt: 8.7, color: RC.sub, x, width: w, factor: 1.15 });
|
write(doc, S, dry, cur, meta, { pt: 9.5, color: RC.ink, x, width: w, factor: 1.1 });
|
||||||
write(doc, S, dry, cur, e.abschluss, { pt: 10.5, style: 'bold', color: RC.ink, x, width: w, factor: 1.2 });
|
write(doc, S, dry, cur, e.abschluss, { pt: 11.5, style: 'bold', color: RC.ink, x, width: w, factor: 1.15 });
|
||||||
if (e.zusatz) write(doc, S, dry, cur, e.zusatz, { pt: 9, color: RC.sub, x, width: w, factor: 1.2 });
|
if (e.zusatz) write(doc, S, dry, cur, e.zusatz, { pt: 9.5, color: RC.ink, x, width: w, factor: 1.2 });
|
||||||
cur.y += 3 * S;
|
cur.y += 2.2 * S;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Centered header: position title (with soft highlight), name, contact row.
|
// Small vector icons drawn in front of the contact rows (phone, mail, pin, web).
|
||||||
|
function drawContactIcon(doc, kind, x, baselineY, S) {
|
||||||
|
const ink = RC.ink;
|
||||||
|
doc.setDrawColor(ink[0], ink[1], ink[2]);
|
||||||
|
doc.setFillColor(ink[0], ink[1], ink[2]);
|
||||||
|
doc.setLineWidth(0.28 * S);
|
||||||
|
const capH = 9 * S * PT * 0.72; // approx. cap height of the contact text
|
||||||
|
const cy = baselineY - capH * 0.5; // visual centre of the line
|
||||||
|
const h = 2.4 * S; // icon height
|
||||||
|
const top = cy - h / 2;
|
||||||
|
switch (kind) {
|
||||||
|
case 'telefon': { // smartphone
|
||||||
|
const w = 1.6 * S;
|
||||||
|
doc.roundedRect(x, top, w, h, 0.4 * S, 0.4 * S, 'S');
|
||||||
|
doc.circle(x + w / 2, top + h - 0.35 * S, 0.22 * S, 'F');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'email': { // envelope
|
||||||
|
const w = 2.6 * S, eh = 1.8 * S, etop = cy - eh / 2;
|
||||||
|
doc.rect(x, etop, w, eh, 'S');
|
||||||
|
doc.line(x, etop, x + w / 2, etop + eh * 0.65);
|
||||||
|
doc.line(x + w, etop, x + w / 2, etop + eh * 0.65);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ort': { // map pin
|
||||||
|
const r = 1.0 * S, cx = x + r, ccy = top + r;
|
||||||
|
doc.circle(cx, ccy, r, 'S');
|
||||||
|
doc.line(cx - r * 0.7, ccy + r * 0.6, cx, top + h);
|
||||||
|
doc.line(cx + r * 0.7, ccy + r * 0.6, cx, top + h);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'web': { // globe
|
||||||
|
const r = 1.2 * S, cx = x + r;
|
||||||
|
doc.circle(cx, cy, r, 'S');
|
||||||
|
doc.ellipse(cx, cy, r * 0.45, r, 'S'); // meridian
|
||||||
|
doc.line(cx - r, cy, cx + r, cy); // equator
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Centered header: position title, name, contact rows with icons.
|
||||||
function composeCvHeader(doc, S, dry, cur, header, titel) {
|
function composeCvHeader(doc, S, dry, cur, header, titel) {
|
||||||
const title = String(titel || '').toUpperCase();
|
const title = String(titel || '').toUpperCase();
|
||||||
if (title) {
|
if (title) {
|
||||||
doc.setFont('helvetica', 'bold');
|
doc.setFont('helvetica', 'bold');
|
||||||
doc.setFontSize(22 * S);
|
doc.setFontSize(22 * S);
|
||||||
const tw = Math.min(doc.getTextWidth(title), RV_FULL);
|
|
||||||
const lineH = 22 * S * PT;
|
const lineH = 22 * S * PT;
|
||||||
if (!dry) {
|
if (!dry) {
|
||||||
const padX = 4 * S;
|
|
||||||
doc.setFillColor(RC.highlight[0], RC.highlight[1], RC.highlight[2]);
|
|
||||||
doc.rect(RV_CENTER - tw / 2 - padX, cur.y + 1.4 * S, tw + padX * 2, lineH * 0.8, 'F');
|
|
||||||
doc.setTextColor(RC.ink[0], RC.ink[1], RC.ink[2]);
|
doc.setTextColor(RC.ink[0], RC.ink[1], RC.ink[2]);
|
||||||
doc.text(title, RV_CENTER, cur.y + lineH * 0.72, { align: 'center', maxWidth: RV_FULL });
|
doc.text(title, RV_CENTER, cur.y + lineH * 0.72, { align: 'center', maxWidth: RV_FULL });
|
||||||
}
|
}
|
||||||
cur.y += lineH * 1.2;
|
cur.y += lineH * 1.2;
|
||||||
}
|
}
|
||||||
centerText(doc, S, dry, cur, header.name, { pt: 13, color: RC.sub, factor: 1.25 });
|
centerText(doc, S, dry, cur, header.name, { pt: 15.5, style: 'bold', color: RC.ink, factor: 1.2 });
|
||||||
cur.y += 2.6 * S;
|
cur.y += 2.6 * S;
|
||||||
|
|
||||||
rule(doc, S, dry, cur.y, RV.mx, PAGE.w - RV.mx, RC.line, 0.4);
|
rule(doc, S, dry, cur.y, RV.mx, PAGE.w - RV.mx, RC.line, 0.4);
|
||||||
cur.y += 3.4 * S;
|
cur.y += 3.4 * S;
|
||||||
const parts = [header.telefon, header.email, cityName(header) || header.ort, header.webseite].filter(Boolean);
|
const items = [
|
||||||
if (parts.length) {
|
header.telefon ? { icon: 'telefon', text: header.telefon } : null,
|
||||||
let cpt = 9;
|
header.email ? { icon: 'email', text: header.email } : null,
|
||||||
doc.setFont('helvetica', 'normal');
|
(cityName(header) || header.ort) ? { icon: 'ort', text: cityName(header) || header.ort } : null,
|
||||||
doc.setFontSize(cpt * S);
|
header.webseite ? { icon: 'web', text: header.webseite } : null,
|
||||||
const contact = parts.join(' | ');
|
].filter(Boolean);
|
||||||
const cw = doc.getTextWidth(contact);
|
if (items.length) {
|
||||||
if (cw > RV_FULL) cpt = Math.max(7.5, cpt * RV_FULL / cw);
|
const cpt = 9;
|
||||||
if (!dry) { doc.setFontSize(cpt * S); doc.setTextColor(RC.sub[0], RC.sub[1], RC.sub[2]); doc.text(contact, RV_CENTER, cur.y + cpt * S * PT * 0.76, { align: 'center' }); }
|
const iconW = 3.4 * S; // icon box width incl. right padding
|
||||||
cur.y += cpt * S * PT * 1.2;
|
for (const it of items) {
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.setFontSize(cpt * S);
|
||||||
|
const tw = doc.getTextWidth(it.text);
|
||||||
|
const totalW = iconW + tw;
|
||||||
|
const leftX = RV_CENTER - totalW / 2;
|
||||||
|
const baseY = cur.y + cpt * S * PT * 0.76;
|
||||||
|
if (!dry) {
|
||||||
|
drawContactIcon(doc, it.icon, leftX, baseY, S);
|
||||||
|
doc.setTextColor(RC.ink[0], RC.ink[1], RC.ink[2]);
|
||||||
|
doc.text(it.text, leftX + iconW, baseY);
|
||||||
|
}
|
||||||
|
cur.y += cpt * S * PT * 1.5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cur.y += 2.8 * S;
|
cur.y += 2.8 * S;
|
||||||
rule(doc, S, dry, cur.y, RV.mx, PAGE.w - RV.mx, RC.line, 0.4);
|
rule(doc, S, dry, cur.y, RV.mx, PAGE.w - RV.mx, RC.line, 0.4);
|
||||||
@@ -645,9 +694,7 @@ function composeCV(doc, S, dry, cur, { cv, header, titel }) {
|
|||||||
if (cv.profil) {
|
if (cv.profil) {
|
||||||
cvHeading(doc, S, dry, cur, 'Profil', RV.mx, RV_FULL);
|
cvHeading(doc, S, dry, cur, 'Profil', RV.mx, RV_FULL);
|
||||||
write(doc, S, dry, cur, cv.profil, { pt: 9.7, color: RC.ink, x: RV.mx, width: RV_FULL, factor: 1.42 });
|
write(doc, S, dry, cur, cv.profil, { pt: 9.7, color: RC.ink, x: RV.mx, width: RV_FULL, factor: 1.42 });
|
||||||
cur.y += 2.5 * S;
|
cur.y += 4 * S;
|
||||||
rule(doc, S, dry, cur.y, RV.mx, PAGE.w - RV.mx, RC.line, 0.4);
|
|
||||||
cur.y += 4.5 * S;
|
|
||||||
}
|
}
|
||||||
const bodyTop = cur.y;
|
const bodyTop = cur.y;
|
||||||
const left = { y: bodyTop }; composeCvLeft(doc, S, dry, left, cv);
|
const left = { y: bodyTop }; composeCvLeft(doc, S, dry, left, cv);
|
||||||
|
|||||||
Reference in New Issue
Block a user