diff --git a/CHANGELOG.md b/CHANGELOG.md index 708c659e..5b3551fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ All notable changes to CV Manager will be documented in this file. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/). +## [1.41.0] - 2026-04-19 + +### Added +- Theme picker now exposes a **Custom section corner radius** option. A toggle reveals a slider (0–32px, integer steps, default 16px) that controls the border-radius of every section box — both built-in (About, Experience, Certifications, Education, Skills, Projects, Timeline) and custom — via the new `--section-radius` CSS variable. When the toggle is off, sections fall back to the pre-1.41 `--radius-lg` value and are visually unchanged. The chosen radius is stored alongside the other theme fields in the per-dataset `data.theme` blob, follows the same `PUT /api/theme` propagation rules (applyToAll, language siblings) and is restored on dataset load. Print styles also honor the custom radius so the printed PDF matches the on-screen look. +- The top CV header's corner radius scales in proportion to the section radius slider (1.5× the section value — matches the baseline 24px header : 16px section ratio) via a new `--header-radius` CSS variable. Picking 0px yields fully square corners on both sections and header; picking 32px yields heavily rounded corners throughout. When the toggle is off, the header keeps its pre-1.41 `--radius-xl` look via the CSS fallback. The scaling applies on the admin preview, the public read-only page, and printed PDFs. + +### Fixed +- When a custom gradient is configured in the theme picker, the `--primary-dark` and `--accent` CSS variables now both follow the user's chosen gradient end color instead of staying derived from the primary. `--primary-dark` drives every item title in the CV (Experience job titles, Certification names, custom-section item titles and bullet titles); `--accent` drives the timeline branch strokes and the item-card highlight pulse. The gradient end is typically the darker of the two endpoints (matching the bundled pair presets), so this keeps body-text titles legible while pulling them inside the chosen palette. When no custom gradient is set, both variables continue to auto-derive from primary as before. +- The admin top toolbar (and its mobile-menu drawer) now use `--header-gradient-start` / `--header-gradient-end` instead of the literal `--primary-dark` / `--dark` variables. In the default theme these resolve to the same colors so the toolbar looks identical, but when a custom gradient is set the toolbar adopts the user's start → end colors — matching the CV header — instead of mixing a custom start color with the default primary-derived dark end. + +## [1.40.0] - 2026-04-19 + +### Added +- Theme picker now exposes a **Custom section title color** option. A toggle reveals a dedicated color wheel with a brightness slider, hex input, and 12 preset swatches chosen to read well as heading text (black, charcoal, slate, gray, navy, dark blue, midnight, dark green, dark red, dark purple, brown, steel). When off, section titles continue to track the primary color as before — the new `--section-title-color` CSS variable falls back to `var(--primary)` so existing themes are visually unchanged. The chosen color applies to every built-in section heading (About, Experience, Certifications, Education, Skills, Projects, Timeline) and every custom section heading on both the admin preview and the public read-only CV page. The color is stored alongside the other theme fields in the per-dataset `data.theme` blob and follows the same propagation rule as the rest of the theme: `applyToAll: true` writes it into every saved dataset; `applyToAll: false` writes it into the current dataset and its language siblings only. + ## [1.39.0] - 2026-04-19 ### Added diff --git a/package-lock.json b/package-lock.json index 3ebbd165..e39b448a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cv-manager", - "version": "1.39.0", + "version": "1.41.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cv-manager", - "version": "1.39.0", + "version": "1.41.0", "dependencies": { "archiver": "^7.0.1", "better-sqlite3": "^9.4.3", diff --git a/package.json b/package.json index 47f04166..0fafb74a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cv-manager", - "version": "1.39.0", + "version": "1.41.0", "description": "Professional CV Management System", "main": "src/server.js", "scripts": { diff --git a/public-readonly/index.html b/public-readonly/index.html index 1710f428..29778c00 100644 --- a/public-readonly/index.html +++ b/public-readonly/index.html @@ -892,7 +892,17 @@

${escapeHtml(proj.title)}

try { bulletStyle = (allSettings && allSettings.themeBulletStyle !== undefined) ? allSettings.themeBulletStyle : (await api('/api/settings/themeBulletStyle')).value; } catch (e) { /* optional */ } - applyThemePublic({ primary: primary || '#0066ff', gradientStart: gradientStart || null, gradientEnd: gradientEnd || null, fontFamily: fontFamily || 'Inter', bulletStyle: bulletStyle || 'triangle' }); + let sectionTitleColor = null; + try { + sectionTitleColor = (allSettings && allSettings.themeSectionTitleColor !== undefined) ? allSettings.themeSectionTitleColor : (await api('/api/settings/themeSectionTitleColor')).value; + } catch (e) { /* optional */ } + let sectionRadius = null; + try { + const raw = (allSettings && allSettings.themeSectionRadius !== undefined) ? allSettings.themeSectionRadius : (await api('/api/settings/themeSectionRadius')).value; + const n = raw == null ? null : parseInt(raw, 10); + if (Number.isInteger(n) && n >= 0 && n <= 32) sectionRadius = n; + } catch (e) { /* optional */ } + applyThemePublic({ primary: primary || '#0066ff', gradientStart: gradientStart || null, gradientEnd: gradientEnd || null, fontFamily: fontFamily || 'Inter', bulletStyle: bulletStyle || 'triangle', sectionTitleColor: sectionTitleColor || null, sectionRadius }); } catch (err) { /* Ignore — defaults stand */ } } @@ -908,6 +918,21 @@

${escapeHtml(proj.title)}

document.body.dataset.bulletStyle = bullet; document.body.dataset.bulletKind = BULLET_GLYPH_IDS.has(bullet) ? 'glyph' : 'icon'; } + const root = document.documentElement; + if (theme.sectionTitleColor && /^#[0-9a-fA-F]{6}$/.test(theme.sectionTitleColor)) { + root.style.setProperty('--section-title-color', theme.sectionTitleColor); + } else { + root.style.removeProperty('--section-title-color'); + } + if (Number.isInteger(theme.sectionRadius) && theme.sectionRadius >= 0 && theme.sectionRadius <= 32) { + root.style.setProperty('--section-radius', theme.sectionRadius + 'px'); + // Header scales 1.5× — matches the baseline 24:16 ratio so the + // top card stays visually consistent with the section boxes. + root.style.setProperty('--header-radius', Math.round(theme.sectionRadius * 1.5) + 'px'); + } else { + root.style.removeProperty('--section-radius'); + root.style.removeProperty('--header-radius'); + } } function applyColorToCSSPublic(hex, gradientStart, gradientEnd, fontFamily) { @@ -915,10 +940,9 @@

${escapeHtml(proj.title)}

const hsl = hexToHSLPublic(hex); root.style.setProperty('--primary', hex); - root.style.setProperty('--primary-dark', hslToHexPublic(hsl.h, hsl.s, Math.max(hsl.l - 15, 10))); root.style.setProperty('--primary-light', hslToHexPublic(hsl.h, Math.min(hsl.s + 10, 100), Math.min(hsl.l + 15, 80))); - const accent = hslToHexPublic((hsl.h + 15) % 360, hsl.s, hsl.l); - root.style.setProperty('--accent', accent); + const autoAccent = hslToHexPublic((hsl.h + 15) % 360, hsl.s, hsl.l); + const autoPrimaryDark = hslToHexPublic(hsl.h, hsl.s, Math.max(hsl.l - 15, 10)); root.style.setProperty('--dark', hslToHexPublic(hsl.h, hsl.s, 15)); root.style.setProperty('--light', hslToHexPublic(hsl.h, 30, 90)); root.style.setProperty('--very-light', hslToHexPublic(hsl.h, 20, 97)); @@ -928,16 +952,23 @@

${escapeHtml(proj.title)}

// so the whole theme feels consistent. const hasCustomGradient = !!(gradientStart || gradientEnd); const gs = gradientStart || hex; - const ge = gradientEnd || accent; + const ge = gradientEnd || autoAccent; root.style.setProperty('--gradient-start', gs); root.style.setProperty('--gradient-end', ge); if (hasCustomGradient) { root.style.setProperty('--header-gradient-start', gs); root.style.setProperty('--header-gradient-end', ge); } else { - root.style.setProperty('--header-gradient-start', hslToHexPublic(hsl.h, hsl.s, Math.max(hsl.l - 15, 10))); + root.style.setProperty('--header-gradient-start', autoPrimaryDark); root.style.setProperty('--header-gradient-end', hslToHexPublic(hsl.h, hsl.s, 15)); } + // When a custom gradient is active, both --primary-dark (item + // titles, cert names, custom-section titles) and --accent (timeline + // branch strokes, item-card highlight pulse) follow the user's + // chosen gradient end so the CV's body text and accent strokes + // stay inside the chosen palette. + root.style.setProperty('--primary-dark', hasCustomGradient ? ge : autoPrimaryDark); + root.style.setProperty('--accent', hasCustomGradient ? ge : autoAccent); const family = fontFamily || 'Inter'; if (family !== 'Inter') ensureFontLoadedPublic(family); root.style.setProperty('--font-family', "'" + family + "', var(--font-family-default)"); diff --git a/public/index.html b/public/index.html index da2b72d5..0488534e 100644 --- a/public/index.html +++ b/public/index.html @@ -127,6 +127,44 @@ +
+ + +
+ +
+ + +
+