fix(cv): enforce readable render palettes
CI and Deploy / test (pull_request) Successful in 4m37s
CI and Deploy / deploy (pull_request) Has been skipped

Choose a contrasting header foreground for custom accents and keep public colour and font overrides inside safe supported values.
This commit is contained in:
cesnimda
2026-08-15 17:08:20 +02:00
parent f0b9b222ff
commit 3b86ea2da0
8 changed files with 98 additions and 18 deletions
+21 -4
View File
@@ -22,6 +22,7 @@ public sealed class ThemedCvRenderer : IThemedCvRenderer
{
settings = CvVariantSettingsJson.Normalize(settings);
var accent = Override(settings.AccentColor, theme.Accent);
var headerInk = ContrastInk(accent);
var headingColor = theme.HeadingColor ?? accent;
var headingFont = Override(settings.HeadingFont, theme.HeadingFont);
var bodyFont = Override(settings.BodyFont, theme.BodyFont);
@@ -35,7 +36,7 @@ public sealed class ThemedCvRenderer : IThemedCvRenderer
? SplitColumns(model, theme, showIcons)
: (string.Empty, RenderSections(model.Sections, theme));
var css = BuildCss(theme, accent, headingColor, headingFont, bodyFont, density, pageDims, twoColumn);
var css = BuildCss(theme, accent, headerInk, headingColor, headingFont, bodyFont, density, pageDims, twoColumn);
var header = RenderHeader(model, theme, showIcons, twoColumn);
var body = theme.Layout switch
{
@@ -189,7 +190,7 @@ public sealed class ThemedCvRenderer : IThemedCvRenderer
return s;
}
private static string BuildCss(CvTheme t, string accent, string headingColor, string headingFont, string bodyFont, double density, (string w, string h) page, bool twoColumn)
private static string BuildCss(CvTheme t, string accent, string headerInk, string headingColor, string headingFont, string bodyFont, double density, (string w, string h) page, bool twoColumn)
{
var margin = F(t.PageMarginMm * density);
var sectionGap = F(t.SectionGapMm * density);
@@ -209,13 +210,14 @@ public sealed class ThemedCvRenderer : IThemedCvRenderer
.sidebar{{background:{t.SidebarBg};color:{t.SidebarInk};padding:{margin}mm;}}
.sidebar .section-title{{color:{t.SidebarInk};border-color:rgba(255,255,255,.35);}}
.sidebar .tag{{border-color:rgba(255,255,255,.4);}}
.sidebar .contact,.sidebar .headline,.sidebar .entry-meta,.sidebar .entry-subtitle{{color:{t.SidebarInk};}}
.sidebar a{{color:inherit;}}
.main{{padding:{margin}mm;}}
.hero .name{{color:{t.SidebarInk};}}"
: $@".main{{padding:0 {margin}mm {margin}mm {margin}mm;}}
.header{{padding:{margin}mm {margin}mm {F(t.SectionGapMm * density)}mm {margin}mm;display:flex;gap:6mm;align-items:center;}}
.header-band{{background:{accent};color:#fff;}}
.header-band .name,.header-band .headline,.header-band a{{color:#fff;}}
.header-band{{background:{accent};color:{headerInk};}}
.header-band .name,.header-band .headline,.header-band .contact,.header-band .contact-item,.header-band a{{color:{headerInk};}}
.header-centered{{flex-direction:column;text-align:center;justify-content:center;}}
.header-centered .contact{{justify-content:center;}}
.header-plain{{border-bottom:2px solid {accent};}}";
@@ -293,6 +295,21 @@ h1,h2{{font-family:{headingFont};}}
$@"<svg viewBox=""0 0 16 16"" fill=""none"" stroke=""currentColor"" stroke-width=""1.3"" stroke-linecap=""round"" stroke-linejoin=""round""><path d=""{path}""/></svg>";
private static string Override(string? value, string fallback) => string.IsNullOrWhiteSpace(value) ? fallback : value.Trim();
private static string ContrastInk(string background)
{
if (background.Length != 7 || background[0] != '#' || !background.Skip(1).All(Uri.IsHexDigit)) return "#000";
var r = Convert.ToInt32(background.Substring(1, 2), 16) / 255d;
var g = Convert.ToInt32(background.Substring(3, 2), 16) / 255d;
var b = Convert.ToInt32(background.Substring(5, 2), 16) / 255d;
static double Channel(double value) => value <= 0.04045 ? value / 12.92 : Math.Pow((value + 0.055) / 1.055, 2.4);
var luminance = 0.2126 * Channel(r) + 0.7152 * Channel(g) + 0.0722 * Channel(b);
var whiteContrast = 1.05 / (luminance + 0.05);
var blackContrast = (luminance + 0.05) / 0.05;
return whiteContrast >= blackContrast ? "#fff" : "#000";
}
private static string F(double v) => v.ToString("0.##", CultureInfo.InvariantCulture);
private static string Enc(string? v) => WebUtility.HtmlEncode(v ?? string.Empty);
private static string Attr(string? v) => WebUtility.HtmlEncode(v ?? string.Empty).Replace("'", "&#39;", StringComparison.Ordinal);