// 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. What stays // in code is the *layout* — the sidebar grid, the DIN-5008 letter, the one-page // auto-scaling. What the user picks is a narrow, curated set of knobs. // // The palette is deliberately closed. A free colour picker invites a neon CV, // which makes an application worse, not better; every option here is a muted // tone that survives being printed in greyscale by an HR department. // Accent colours. `ink`/`sub`/`hair` stay neutral in every theme — the accent // only carries the name, section labels, company names and markers, exactly as // the monochrome original did. const AKZENTE = [ { key: 'anthrazit', label: 'Anthrazit (Standard)', 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] }, ]; 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 (Standard)' }, { 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ß)' }, ]; // Layout is fixed for now; the field exists so a second variant can be added // later without another migration. const LAYOUTS = [ { key: 'sidebar', label: 'Sidebar (Standard)' }, ]; 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 }, { 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))); } 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. function settings(overrides = {}) { const out = { ...DEFAULTS }; for (const f of FELDER) { 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; let sidebarBg; if (s.sidebar === 'weiss') sidebarBg = [255, 255, 255]; else if (s.sidebar === 'getoent') sidebarBg = tint(accent, 0.08); else sidebarBg = NEUTRAL_SIDEBAR; return { layout: s.layout, // Résumé palette — neutral base, configurable accent. rc: { ink: [33, 33, 33], sub: [92, 96, 100], hair: [208, 210, 214], accent, sidebarBg, track: [199, 202, 208], onSide: [38, 40, 44], onSideSub: [96, 100, 106], }, // Cover-letter palette — same accent, so both documents read as one set. lc: { ink: [26, 26, 26], muted: [92, 96, 100], hair: [206, 208, 212], accent, }, 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. function list(overrides = {}) { const s = settings(overrides); return FELDER.map((f) => ({ key: f.key, label: f.label, aktuell: s[f.key], optionen: f.optionen.map((o) => ({ key: o.key, label: o.label, gewaehlt: o.key === s[f.key] })), })); } // True when the user deviates from the shipped defaults. function isAngepasst(overrides = {}) { const s = settings(overrides); return Object.keys(DEFAULTS).some((k) => s[k] !== DEFAULTS[k]); } module.exports = { DEFAULTS, AKZENTE, FELDER, settings, resolve, list, isAngepasst, // The default theme, for callers that pass no overrides at all. defaultTheme: () => resolve({}), };