Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
43 changes: 37 additions & 6 deletions public-readonly/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -892,7 +892,17 @@ <h3 class="project-title" itemprop="name">${escapeHtml(proj.title)}</h3>
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 */ }
}

Expand All @@ -908,17 +918,31 @@ <h3 class="project-title" itemprop="name">${escapeHtml(proj.title)}</h3>
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) {
const root = document.documentElement;
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));
Expand All @@ -928,16 +952,23 @@ <h3 class="project-title" itemprop="name">${escapeHtml(proj.title)}</h3>
// 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)");
Expand Down
38 changes: 38 additions & 0 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,44 @@
</div>
</div>

<div class="theme-section">
<label class="theme-section-row">
<input type="checkbox" id="themeUseSectionTitleColor">
<span data-i18n="theme.use_section_title_color">Custom section title color</span>
</label>
<div class="theme-section-title-picker" id="sectionTitleColorWrapper" style="display:none;">
<div class="color-wheel-container">
<canvas id="sectionTitleWheel" width="180" height="180"></canvas>
<div class="color-wheel-cursor" id="sectionTitleWheelCursor"></div>
</div>
<div class="color-brightness">
<label data-i18n="theme.brightness">Brightness</label>
<input type="range" id="sectionTitleBrightness" min="5" max="70" value="20">
</div>
<div class="color-preview-row">
<div class="color-preview" id="sectionTitlePreview"></div>
<input type="text" class="color-hex-input" id="sectionTitleHexInput" value="#000000" maxlength="7">
</div>
<div class="color-presets" id="sectionTitlePresets"></div>
</div>
</div>

<div class="theme-section">
<label class="theme-section-row">
<input type="checkbox" id="themeUseSectionRadius">
<span data-i18n="theme.use_section_radius">Custom section corner radius</span>
</label>
<div class="theme-section-radius-picker" id="sectionRadiusWrapper" style="display:none;">
<div class="color-brightness">
<label>
<span data-i18n="theme.section_radius">Corner radius</span>
<span class="theme-section-radius-value" id="themeSectionRadiusValue">16px</span>
</label>
<input type="range" id="themeSectionRadiusSlider" min="0" max="32" step="1" value="16">
</div>
</div>
</div>

<div class="theme-section">
<label class="theme-section-row">
<input type="checkbox" id="themeApplyToAll" checked>
Expand Down
9 changes: 7 additions & 2 deletions public/shared/admin.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
left: 0;
right: 0;
height: 54px;
background: linear-gradient(135deg, var(--primary-dark), var(--dark));
/* Mirrors the CV header gradient — in default mode the auto-derived
endpoints resolve to the same primary-dark → dark colors used pre-1.41,
so existing themes look identical. When the user picks a custom
gradient, the toolbar follows their start/end the same way the CV
header does. */
background: linear-gradient(135deg, var(--header-gradient-start), var(--header-gradient-end));
display: flex;
align-items: center;
justify-content: space-between;
Expand Down Expand Up @@ -2274,7 +2279,7 @@
flex-direction: column;
gap: 2px;
padding: 8px 12px;
background: linear-gradient(135deg, var(--primary-dark), var(--dark));
background: linear-gradient(135deg, var(--header-gradient-start), var(--header-gradient-end));
box-shadow: var(--shadow-lg);
z-index: 999;
border-top: 1px solid rgba(255,255,255,0.1);
Expand Down
Loading
Loading