Design der Unterlagen unter Vorlagen einstellbar
Akzentfarbe, Sidebar-Hintergrund, Foto (an/aus, eckig/rund) und Schriftgroesse lagen als Konstanten im Renderer. Sie kommen jetzt aus lib/design.js und sind unter Vorlagen waehlbar, inklusive Vorschau-PDF mit Musterinhalten - die zeigt das Ergebnis, ohne dass die KI laufen muss. Bewusst geschlossen gehalten: die Palette ist eine kurze Liste gedeckter Toene (kein freier Color-Picker), Grauwerte und Grundlayout bleiben fest. Die Schriftgroesse ist nur der Startwert der Seitenskalierung - der Lebenslauf passt weiterhin garantiert auf eine Seite. Das Theme wird durch die Render-Funktionen gereicht statt als Modulzustand gesetzt, damit spaeter mehrere Bewerber parallel generieren koennen, ohne sich gegenseitig das Design umzustellen. Rundes Foto: das Bild fuellt den Kreis formatfuellend und zentriert (Clip statt Skalieren), sonst wuerde ein Hochformat-Foto im Quadrat gestaucht. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+161
@@ -0,0 +1,161 @@
|
||||
// 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({}),
|
||||
};
|
||||
Reference in New Issue
Block a user