From 0301a1c430a5b4b6215aaedd0dbe2374c482c7ab Mon Sep 17 00:00:00 2001 From: Thomas Hackner Date: Tue, 14 Jul 2026 18:54:18 +0200 Subject: [PATCH] =?UTF-8?q?Sunny:=20Anschreiben=20dezenter=20=E2=80=94=20F?= =?UTF-8?q?arbe=20nur=20noch=20im=20Kopfband?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Das Anschreiben war ein zweites Poster: Konfetti im Band, solides Akzent-Badge, Betreff und Anrede beide in der Akzentfarbe, ein pastellener Marker-Strich hinter dem Namen und eine Farbleiste als Fußzeile. Neben dem Lebenslauf las sich das als Lärm, nicht als Set. Jetzt trägt die Seite genau eine Farbfläche — das Kopfband —, alles darunter ist Typografie: - Kopfband ohne Konfetti, Name 27pt -> 22pt - Rollen-Badge als weiße Pille mit Akzentschrift (der Lebenslauf behält sein solides Badge; die Umstellung sitzt in der Letter-Palette) - Datum als schlichte rechtsbündige Zeile statt Pastell-Chip - Betreff und Anrede in Tinte; die Bar über dem Betreff bleibt der einzige Farbakzent im Textkörper - Signatur ohne Bild: Name über feinem Haarstrich statt Marker-Swipe - Fußzeile: Haarlinie mit gedämpftem Kontakttext statt Farbleiste suKonfetti/suDot hatten damit keinen Aufrufer mehr und fallen weg. Co-Authored-By: Claude Opus 4.8 --- lib/design.js | 8 +++- lib/documents.js | 98 +++++++++++++++++++----------------------------- 2 files changed, 45 insertions(+), 61 deletions(-) diff --git a/lib/design.js b/lib/design.js index 354acb6..f200d70 100644 --- a/lib/design.js +++ b/lib/design.js @@ -250,8 +250,12 @@ function resolve(overrides = {}) { cardBorder: rc.cardBorder, deko: rc.deko, band: rc.band, - pillBg: rc.pillBg, - pillInk: rc.pillInk, + // The letter's role badge sits on the pastel head band, so a solid accent + // slug reads too loud next to the résumé's poster. A white pill with the + // accent knocked out keeps the motif but softens it — the letter is the + // quiet twin, not a second poster. + pillBg: rc.cardBg, + pillInk: accent, }, }; const lc = LC[s.layout] || LC.sidebar; diff --git a/lib/documents.js b/lib/documents.js index dd233c4..fd2baee 100644 --- a/lib/documents.js +++ b/lib/documents.js @@ -1573,27 +1573,6 @@ function suBar(doc, p, S, dry, y, align, frac = 0.62) { doc.rect(x, y, w, 2.4 * S, 'F'); } -// A filled dot. The confetti scattered through the Sunny documents is built from -// these — a designer's flourish, kept to the margins where no text can collide. -function suDot(doc, cx, cy, r, color) { - doc.setFillColor(color[0], color[1], color[2]); - doc.circle(cx, cy, r, 'F'); -} - -// Confetti in the band's gutters — the strips between its edges and the text -// column, empty by construction, so the decoration can never run into a word. -// -// It all lives on the *right*: dots down the left gutter sat directly beside the -// name and read as bullet points in front of it, not as decoration. The only -// thing on the left is a sparkle low in the band, well clear of the name. -function suKonfetti(doc, p, S, bandY, bandH) { - const gx = SU.r - 3 * S; - suDot(doc, gx, bandY + bandH * 0.26, 1.4 * S, p.accent); - suDot(doc, gx, bandY + bandH * 0.48, 0.9 * S, [255, 255, 255]); - suDot(doc, gx, bandY + bandH * 0.66, 1.9 * S, [255, 255, 255]); - sparkle(doc, SU.mx + 3.4 * S, bandY + bandH - 5.5 * S, 4.4 * S, [255, 255, 255]); -} - // Display heading: the poster type. Set as large as the layout's rhythm allows // and shrunk only if a long word (Berufsausbildung…) would not otherwise fit. function suHeading(doc, t, S, dry, cur, title, align = 'left') { @@ -2277,7 +2256,7 @@ function composeLetterSunny(doc, t, S, dry, cur, { letter, header, job, anlagen, const cl = { y: top }; const name = String(header.name || ''); - let namePt = 27; + let namePt = 22; doc.setFont(doc.__sans, 'bold'); for (; namePt > 14; namePt -= 0.5) { doc.setFontSize(namePt * S); @@ -2308,24 +2287,27 @@ function composeLetterSunny(doc, t, S, dry, cur, { letter, header, job, anlagen, if (!dry) { doc.setFillColor(t.lc.band[0], t.lc.band[1], t.lc.band[2]); doc.rect(SU.mx, bandY, SU.w, bandH, 'F'); - suKonfetti(doc, t.lc, S, bandY, bandH); inner({ y: bandY }, false); } cur.y = bandY + bandH + 9 * S; - // --- Recipient (Anschriftfeld) with the date as a pastel chip on the same - // line — DIN keeps its address block, the date just stops being a lonely - // right-aligned string and becomes part of the layout's chip language. + // --- Recipient (Anschriftfeld) with the date on the same line — DIN keeps its + // address block; the date sits right-aligned in the accent, a quiet line + // rather than a pastel chip, so the letter's head is the only block of + // colour on the page. const emp = letter.empfaenger || {}; const empFirma = emp.firma || job.firma || ''; const empOrt = emp.ort || job.ort || ''; const datum = stadt ? `${stadt}, ${today}` : today; if (!dry) { - const p = pill(doc, t, S, true, 0, 0, datum, { pt: 8.6, style: 'normal' }); - pill(doc, t, S, false, right - p.w, cur.y - 0.5 * S, datum, - { pt: 8.6, style: 'normal', bg: t.lc.band, ink: t.lc.accent }); + doc.setFont(doc.__sans, 'normal'); + doc.setFontSize(8.6 * S); + doc.setTextColor(t.lc.accent[0], t.lc.accent[1], t.lc.accent[2]); + // Align the date's baseline with the first recipient line so the two read + // as one row rather than two floats at different heights. + doc.text(datum, right, cur.y + 10.2 * S * PT * 0.76, { align: 'right' }); } - const empW = width * 0.6; // keep the address clear of the date chip + const empW = width * 0.6; // keep the address clear of the date line if (empFirma) write(doc, S, dry, cur, empFirma, { pt: 10.2, style: 'bold', color: t.lc.ink, x, width: empW, factor: 1.32 }); if (emp.ansprechpartner) write(doc, S, dry, cur, `z. Hd. ${emp.ansprechpartner}`, { pt: 10.2, color: t.lc.ink, x, width: empW, factor: 1.32 }); if (emp.adresse) write(doc, S, dry, cur, emp.adresse, { pt: 10.2, color: t.lc.ink, x, width: empW, factor: 1.32 }); @@ -2337,13 +2319,13 @@ function composeLetterSunny(doc, t, S, dry, cur, { letter, header, job, anlagen, cur.y += 9 * S; suBar(doc, t.lc, S, dry, cur.y, 'left', 0.34); cur.y += 2.4 * S + 4.2 * S; - write(doc, S, dry, cur, betreff, { pt: 15, style: 'bold', color: t.lc.accent, x, width, factor: 1.18 }); + write(doc, S, dry, cur, betreff, { pt: 13.5, style: 'bold', color: t.lc.ink, x, width, factor: 1.18 }); - // --- Salutation + body. The salutation carries the accent: it is the moment - // the letter starts speaking, and it echoes the coloured entry titles of - // the résumé. + // --- Salutation + body. The salutation is set bold in ink rather than the + // accent — the bar above the subject is the page's one colour cue, and the + // letter's voice stays quiet so the résumé can be the loud twin. cur.y += 5.5 * S; - if (letter.anrede) { write(doc, S, dry, cur, letter.anrede, { pt: 10.2, style: 'bold', color: t.lc.accent, x, width, factor: 1.4 }); cur.y += 2.8 * S; } + if (letter.anrede) { write(doc, S, dry, cur, letter.anrede, { pt: 10.2, style: 'bold', color: t.lc.ink, x, width, factor: 1.4 }); cur.y += 2.8 * S; } letter.absaetze.forEach((p, i) => { if (i > 0) cur.y += 3 * S; write(doc, S, dry, cur, p, { pt: 10.2, color: t.lc.ink, x, width, factor: 1.52 }); @@ -2369,24 +2351,21 @@ function composeLetterSunny(doc, t, S, dry, cur, { letter, header, job, anlagen, if (!dry) doc.addImage(signatur.dataUrl, signatur.format || 'PNG', x, cur.y, sigW, sigH); cur.y += sigH; } else { - // No signature image: the typed name gets a pastel highlight swiped behind - // it, like a marker stroke — the flourish that stands in for the missing - // handwriting instead of leaving an empty gap. + // No signature image: the typed name stands alone in the accent with a + // short hairline beneath it — a quiet flourish in place of the missing + // handwriting, instead of the poster's loud pastel marker swipe. cur.y += 12 * S; const pt = 10.6; + const baseY = cur.y + pt * S * PT * 0.76; + write(doc, S, dry, cur, header.name, { pt, style: 'bold', color: t.lc.accent, x, width, factor: 1.25 }); if (!dry) { doc.setFont(doc.__sans, 'bold'); doc.setFontSize(pt * S); - const w = doc.getTextWidth(String(header.name || '')); - // Centre the swipe on the glyphs: `write` puts the baseline at - // cur.y + pt*PT*0.76, so a box hung off cur.y alone sits too low and the - // capitals poke out over its top edge. - const h = pt * S * PT * 1.2; - const y = cur.y + pt * S * PT * 0.76 - h * 0.72; - doc.setFillColor(t.lc.band[0], t.lc.band[1], t.lc.band[2]); - doc.roundedRect(x - 1.8 * S, y, w + 3.6 * S, h, 1.4 * S, 1.4 * S, 'F'); + const w = Math.min(doc.getTextWidth(String(header.name || '')), 42 * S); + doc.setDrawColor(t.lc.accent[0], t.lc.accent[1], t.lc.accent[2]); + doc.setLineWidth(0.35 * S); + doc.line(x, baseY + 1.6 * S, x + w, baseY + 1.6 * S); } - write(doc, S, dry, cur, header.name, { pt, style: 'bold', color: t.lc.accent, x, width, factor: 1.25 }); } // --- Enclosures as chips, in the same pill language as the contact details --- @@ -2401,7 +2380,10 @@ function composeLetterSunny(doc, t, S, dry, cur, { letter, header, job, anlagen, cur.y = c.y; } - // --- Footer: the contact strip on a pastel bar, in the reserved bottom margin --- + // --- Footer: a single hairline above a quiet, centred contact line — the + // letter's footer stays as contact information, not a second band of + // colour echoing the head. The poster résumé can carry the dots; the + // letter closes with a rule instead. if (!dry) { const parts = []; if (header.adresse) { @@ -2413,17 +2395,15 @@ function composeLetterSunny(doc, t, S, dry, cur, { letter, header, job, anlagen, if (parts.length) { const fh = 7.5 * S; const fy = PAGE.h - SU.bottom - fh; - doc.setFillColor(t.lc.band[0], t.lc.band[1], t.lc.band[2]); - doc.rect(SU.mx, fy, SU.w, fh, 'F'); - // Dots closing the strip at both ends — the head's confetti, answered. - suDot(doc, SU.mx + 4 * S, fy + fh / 2, 1.1 * S, t.lc.accent); - suDot(doc, SU.r - 4 * S, fy + fh / 2, 1.1 * S, t.lc.accent); + doc.setDrawColor(t.lc.hair[0], t.lc.hair[1], t.lc.hair[2]); + doc.setLineWidth(0.4 * S); + doc.line(SU.mx, fy, SU.r, fy); - // Fit the strip to the band. A long address plus a long e-mail address - // overruns it at a fixed size — the line then slides out over the dots and - // off the band's edge. Shrink to fit; if even the floor is too wide, drop - // the postal address and keep the two ways to actually reach the person. - const raum = SU.w - 16 * S; // band minus the dots at both ends + // Fit the strip to the line. A long address plus a long e-mail address + // overruns it at a fixed size. Shrink to fit; if even the floor is too + // wide, drop the postal address and keep the two ways to actually reach + // the person. + const raum = SU.w - 6 * S; doc.setFont(doc.__sans, 'normal'); const passt = (text, pt) => { doc.setFontSize(pt * S); @@ -2442,7 +2422,7 @@ function composeLetterSunny(doc, t, S, dry, cur, { letter, header, job, anlagen, if (fpt === null) fpt = 5.4; // Notnagel: lieber klein als über den Rand doc.setFontSize(fpt * S); - doc.setTextColor(t.lc.accent[0], t.lc.accent[1], t.lc.accent[2]); + doc.setTextColor(t.lc.muted[0], t.lc.muted[1], t.lc.muted[2]); doc.text(strip, PAGE.w / 2, fy + fh / 2 + fpt * S * PT * 0.35, { align: 'center' }); } }