fix(cv): enforce readable render palettes
Choose a contrasting header foreground for custom accents and keep public colour and font overrides inside safe supported values.
This commit is contained in:
@@ -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("'", "'", StringComparison.Ordinal);
|
||||
|
||||
Reference in New Issue
Block a user