// Editable design settings for the generated application documents. // // Same contract as lib/prompts.js: the defaults live here, an override is a row // in the `design` table, and deleting the row restores the default. // // Two layouts ship today: // // sidebar — the classic: neutral greys, tinted sidebar, DIN-5008 letter. The // accent is a single restrained colour; hierarchy comes from weight // and tracking. Right for almost every office application. // // social — a playful media-kit look (rounded cards, pills, hearts, a script // headline, a full-page tinted background). Right for creative, // social-media and design-adjacent roles — and deliberately loud. // // The cover letter always follows the résumé's layout, so both documents arrive // as one coherent set — that is the whole point of picking a design here. // // The palette stays closed (no free colour picker): every tone is one that still // works when an HR department prints it in greyscale. // Accent colours. In `sidebar` the accent is the *only* colour; in `social` the // whole page palette (background, cards, pills) is derived from it, so the // playful layout works in pink as well as in deep blue. const AKZENTE = [ { key: 'anthrazit', label: 'Anthrazit', rgb: [23, 23, 23] }, { key: 'tiefblau', label: 'Tiefblau', rgb: [30, 58, 95] }, { key: 'bordeaux', label: 'Bordeaux', rgb: [122, 31, 47] }, { key: 'waldgruen', label: 'Waldgrün', rgb: [27, 67, 50] }, { key: 'stahlblau', label: 'Stahlblau', rgb: [51, 65, 85] }, { key: 'kupfer', label: 'Kupfer', rgb: [124, 62, 24] }, { key: 'pink', label: 'Pink (verspielt)', rgb: [232, 54, 143] }, { key: 'violett', label: 'Violett (verspielt)', rgb: [147, 51, 187] }, ]; const LAYOUTS = [ { key: 'sidebar', label: 'Klassisch mit Sidebar (Standard)' }, { key: 'social', label: 'Social Media (verspielt)' }, ]; const SIDEBAR_VARIANTEN = [ { key: 'neutral', label: 'Neutral grau (Standard)' }, { key: 'getoent', label: 'Im Akzent getönt' }, { key: 'weiss', label: 'Weiß (ohne Band)' }, ]; const FOTO_FORMEN = [ { key: 'eckig', label: 'Eckig' }, { key: 'rund', label: 'Rund' }, ]; const SCHRIFT_STUFEN = [ { key: '90', label: '90 % (kompakt)' }, { key: '95', label: '95 %' }, { key: '100', label: '100 % (Standard)' }, { key: '105', label: '105 %' }, { key: '110', label: '110 % (groß)' }, ]; // Defaults per layout: the playful layout is pink and round by default, because // anthracite-and-square would defeat the point of choosing it. Any of these can // still be overridden individually. const LAYOUT_DEFAULTS = { sidebar: { akzent: 'anthrazit', foto_form: 'eckig' }, social: { akzent: 'pink', foto_form: 'rund' }, }; const DEFAULTS = { layout: 'sidebar', akzent: 'anthrazit', sidebar: 'neutral', foto_anzeigen: '1', foto_form: 'eckig', schrift: '100', }; const FELDER = [ { key: 'layout', label: 'Layout', optionen: LAYOUTS }, { key: 'akzent', label: 'Akzentfarbe', optionen: AKZENTE }, { key: 'sidebar', label: 'Sidebar-Hintergrund', optionen: SIDEBAR_VARIANTEN, nurLayout: 'sidebar' }, { key: 'foto_form', label: 'Form des Bewerberfotos', optionen: FOTO_FORMEN }, { key: 'schrift', label: 'Schriftgröße', optionen: SCHRIFT_STUFEN }, ]; const NEUTRAL_SIDEBAR = [242, 243, 245]; // Mix a colour towards white. amount = 0 -> white, 1 -> the colour itself. function tint(rgb, amount) { return rgb.map((c) => Math.round(c * amount + 255 * (1 - amount))); } // Mix a colour towards black. function shade(rgb, amount) { return rgb.map((c) => Math.round(c * amount)); } function findOption(list, key, fallbackKey) { return list.find((o) => o.key === key) || list.find((o) => o.key === fallbackKey); } // A checkbox paired with a hidden fallback submits *both* values, which Express // hands over as an array — the last entry is the effective one. function one(v) { return Array.isArray(v) ? v[v.length - 1] : v; } // Fold the stored overrides onto the defaults, dropping unknown keys/values so a // stale row, a hand-crafted query string or a checkbox pair can never produce an // invalid theme. Fields not explicitly set fall back to the *layout's* default, // which is why switching to "social" turns pink without touching anything else. function settings(overrides = {}) { const layoutRaw = one(overrides.layout); const layout = LAYOUTS.some((l) => l.key === layoutRaw) ? String(layoutRaw) : DEFAULTS.layout; const out = { ...DEFAULTS, ...(LAYOUT_DEFAULTS[layout] || {}), layout }; for (const f of FELDER) { if (f.key === 'layout') continue; const v = one(overrides[f.key]); if (v && f.optionen.some((o) => o.key === String(v))) out[f.key] = String(v); } const foto = one(overrides.foto_anzeigen); if (foto === '0' || foto === '1') out.foto_anzeigen = foto; return out; } // Build the concrete theme the renderer draws with. Everything the PDF code // needs to know about the user's choices is in here — the renderer itself has // no idea these are configurable. function resolve(overrides = {}) { const s = settings(overrides); const accent = findOption(AKZENTE, s.akzent, 'anthrazit').rgb; const social = s.layout === 'social'; let sidebarBg; if (s.sidebar === 'weiss') sidebarBg = [255, 255, 255]; else if (s.sidebar === 'getoent') sidebarBg = tint(accent, 0.08); else sidebarBg = NEUTRAL_SIDEBAR; // The playful layout derives its whole palette from the accent: a tinted page, // near-white cards with a soft border, pill chips, and a warm dark ink that is // the accent shaded down rather than plain black. const rc = social ? { ink: shade(accent, 0.35), sub: tint(shade(accent, 0.55), 0.75), hair: tint(accent, 0.30), accent, pageBg: tint(accent, 0.10), cardBg: [255, 255, 255], cardBorder: tint(accent, 0.28), // Sparkles/blob sit on the tinted page, so they need more punch than the // card border or they simply vanish. deko: tint(accent, 0.45), pillBg: tint(accent, 0.18), pillInk: shade(accent, 0.6), sidebarBg, // unused in this layout, kept so the shape stays stable track: tint(accent, 0.45), onSide: shade(accent, 0.35), onSideSub: tint(shade(accent, 0.55), 0.75), } : { ink: [33, 33, 33], sub: [92, 96, 100], hair: [208, 210, 214], accent, pageBg: null, // no page tint in the classic layout cardBg: [255, 255, 255], cardBorder: [208, 210, 214], deko: [208, 210, 214], // the classic layout draws no sparkles; kept for shape parity pillBg: tint(accent, 0.12), pillInk: accent, sidebarBg, track: [199, 202, 208], onSide: [38, 40, 44], onSideSub: [96, 100, 106], }; // The cover letter always mirrors the résumé, so the two documents read as one // set — a playful CV never arrives with a stiff monochrome letter. const lc = social ? { ink: rc.ink, muted: rc.sub, hair: rc.hair, accent, pageBg: rc.pageBg, cardBg: rc.cardBg, cardBorder: rc.cardBorder, deko: rc.deko, pillBg: rc.pillBg, pillInk: rc.pillInk, } : { ink: [26, 26, 26], muted: [92, 96, 100], hair: [206, 208, 212], accent, pageBg: null, cardBg: [255, 255, 255], cardBorder: [206, 208, 212], deko: [206, 208, 212], pillBg: tint(accent, 0.12), pillInk: accent, }; return { layout: s.layout, // Which typefaces the renderer registers and draws with. The playful layout // uses a geometric sans plus a script face for the "Hallo, ich bin" line; // the classic one stays on Lato and embeds nothing extra. fonts: social ? { sans: 'Poppins', script: 'Pacifico' } : { sans: 'Lato', script: null }, rc, lc, foto: { anzeigen: s.foto_anzeigen !== '0', rund: s.foto_form === 'rund', }, // Starting scale for the one-page fit. The auto-scaler may still shrink // below this when there is a lot of content — it never grows past it. scale: (Number(s.schrift) || 100) / 100, }; } // Field list for the Vorlagen UI, with the current selection marked. Fields that // only apply to one layout carry `nurLayout` so the form can hide them. function list(overrides = {}) { const s = settings(overrides); return FELDER.map((f) => ({ key: f.key, label: f.label, aktuell: s[f.key], nurLayout: f.nurLayout || null, optionen: f.optionen.map((o) => ({ key: o.key, label: o.label, gewaehlt: o.key === s[f.key] })), })); } // True when the user deviates from what the chosen layout would ship with. function isAngepasst(overrides = {}) { const s = settings(overrides); const basis = { ...DEFAULTS, ...(LAYOUT_DEFAULTS[s.layout] || {}), layout: s.layout }; return s.layout !== DEFAULTS.layout || Object.keys(basis).some((k) => s[k] !== basis[k]); } module.exports = { DEFAULTS, LAYOUT_DEFAULTS, AKZENTE, LAYOUTS, FELDER, settings, resolve, list, isAngepasst, defaultTheme: () => resolve({}), };