feat(cv): extend templates and extraction
This commit is contained in:
@@ -28,6 +28,7 @@ public sealed class CvTheme
|
||||
// Typography
|
||||
public string HeadingFont { get; init; } = "Georgia, 'Times New Roman', serif";
|
||||
public string BodyFont { get; init; } = "Arial, Helvetica, sans-serif";
|
||||
public string UtilityFont { get; init; } = "'Roboto Mono', Consolas, monospace";
|
||||
public double NameSizePt { get; init; } = 24;
|
||||
public double HeadingSizePt { get; init; } = 12;
|
||||
public double BodySizePt { get; init; } = 10;
|
||||
@@ -51,6 +52,15 @@ public sealed class CvTheme
|
||||
// the single-column themes; surfaced in GET /api/cv/themes so the picker can badge it.
|
||||
public bool AtsFriendly { get; init; }
|
||||
public bool Premium { get; init; }
|
||||
public bool NumberSections { get; init; }
|
||||
|
||||
// Builder options this template intentionally exposes. The renderer remains generic; the
|
||||
// registry tells the editor which controls make sense for a selected template.
|
||||
public List<string> SupportedSettings { get; init; } = new()
|
||||
{
|
||||
"accent", "typography", "spacing", "skills", "page", "heading", "header",
|
||||
"layout", "sidebar", "photo", "icons",
|
||||
};
|
||||
|
||||
// Which section keys render in the sidebar for two-column layouts (ignored for single/header-band).
|
||||
public List<string> SidebarSections { get; init; } = new() { "contact", "skills", "languages" };
|
||||
@@ -58,7 +68,7 @@ public sealed class CvTheme
|
||||
|
||||
public static class CvThemeCatalog
|
||||
{
|
||||
// 8 professional themes, all data. To add one: append here.
|
||||
// Professional themes, all data. To add one: append here.
|
||||
public static readonly IReadOnlyList<CvTheme> Themes = new List<CvTheme>
|
||||
{
|
||||
new()
|
||||
@@ -134,6 +144,18 @@ public static class CvThemeCatalog
|
||||
PhotoShape = "circle", DefaultIcons = true,
|
||||
SidebarSections = new() { "contact", "skills", "languages", "interests" },
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = "code", Name = "Code", Category = "Technical",
|
||||
Description = "Developer-focused editorial layout with numbered sections and precise mono details.",
|
||||
Layout = "single", HeaderStyle = "plain", HeadingStyle = "caps-rule",
|
||||
Accent = "#0b7a63", Ink = "#171a20", Muted = "#555e6b", Line = "#d8dde3", Paper = "#ffffff",
|
||||
HeadingFont = "'Segoe UI', Roboto, Arial, sans-serif", BodyFont = "'Segoe UI', Roboto, Arial, sans-serif",
|
||||
UtilityFont = "Consolas, 'Courier New', monospace", NameSizePt = 25.5, HeadingSizePt = 9.5,
|
||||
BodySizePt = 9.4, LineHeight = 1.43, PageMarginMm = 15.24, SectionGapMm = 5.6, EntryGapMm = 4.3,
|
||||
PhotoShape = "none", DefaultIcons = false, AtsFriendly = true, NumberSections = true,
|
||||
SupportedSettings = new() { "accent", "typography", "spacing", "skills", "page" },
|
||||
},
|
||||
};
|
||||
|
||||
public static CvTheme Resolve(string? id)
|
||||
|
||||
@@ -34,7 +34,8 @@ public sealed class CvVariantSettings
|
||||
public double? EntryGapMm { get; set; }
|
||||
public string? HeadingStyle { get; set; } // caps-rule | underline | plain | bar
|
||||
public string? HeaderStyle { get; set; } // plain | band | centered | kicker
|
||||
public string? SkillsStyle { get; set; } // tags | text
|
||||
public string? SkillsStyle { get; set; } // tags | text | bullets | grouped
|
||||
public List<CvSkillGroupSetting>? SkillGroups { get; set; }
|
||||
public string? Layout { get; set; } // single | sidebar-left | sidebar-right | header-band
|
||||
public double? SidebarWidthMm { get; set; }
|
||||
public List<string>? SidebarSections { get; set; }
|
||||
@@ -62,6 +63,9 @@ public sealed class CvSectionSetting
|
||||
// master order and come last. Null => master profile order. Lets a variant reorder entries without
|
||||
// touching the master profile.
|
||||
public List<string>? ItemOrder { get; set; }
|
||||
// Optional CV-specific content for bullet/tag sections. Null uses master data; an empty list is
|
||||
// an intentional empty override. Entry sections continue to use stable per-item overrides.
|
||||
public List<string>? Items { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CvItemOverride
|
||||
@@ -78,6 +82,14 @@ public sealed class CvCustomSectionSetting
|
||||
public string? Title { get; set; }
|
||||
public List<string> Items { get; set; } = new();
|
||||
public bool Hidden { get; set; }
|
||||
public string? PresetKey { get; set; }
|
||||
public string? ContentType { get; set; } // paragraphs | bullets | entries
|
||||
}
|
||||
|
||||
public sealed class CvSkillGroupSetting
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public List<string> Items { get; set; } = new();
|
||||
}
|
||||
|
||||
public static class CvVariantSettingsJson
|
||||
@@ -128,7 +140,7 @@ public static class CvVariantSettingsJson
|
||||
s.SidebarWidthMm = Clamp(s.SidebarWidthMm, 45, 85);
|
||||
s.HeadingStyle = NormalizeChoice(s.HeadingStyle, "caps-rule", "underline", "plain", "bar");
|
||||
s.HeaderStyle = NormalizeChoice(s.HeaderStyle, "plain", "band", "centered", "kicker");
|
||||
s.SkillsStyle = NormalizeChoice(s.SkillsStyle, "tags", "text");
|
||||
s.SkillsStyle = NormalizeChoice(s.SkillsStyle, "tags", "text", "bullets", "grouped");
|
||||
s.Layout = NormalizeChoice(s.Layout, "single", "sidebar-left", "sidebar-right", "header-band");
|
||||
s.DateFormat = NormalizeChoice(s.DateFormat, "long", "short", "numeric", "year");
|
||||
if (!string.IsNullOrWhiteSpace(s.Language))
|
||||
@@ -146,6 +158,21 @@ public static class CvVariantSettingsJson
|
||||
s.Sections ??= new();
|
||||
s.Overrides ??= new();
|
||||
s.CustomSections ??= new();
|
||||
s.SkillGroups = s.SkillGroups?
|
||||
.Where(group => !string.IsNullOrWhiteSpace(group.Name) || group.Items.Any(item => !string.IsNullOrWhiteSpace(item)))
|
||||
.Take(30)
|
||||
.Select(group => new CvSkillGroupSetting
|
||||
{
|
||||
Name = (group.Name ?? string.Empty).Trim(),
|
||||
Items = group.Items.Where(item => !string.IsNullOrWhiteSpace(item)).Select(item => item.Trim()).Distinct(StringComparer.OrdinalIgnoreCase).Take(100).ToList(),
|
||||
})
|
||||
.ToList();
|
||||
foreach (var section in s.CustomSections)
|
||||
{
|
||||
section.ContentType = NormalizeChoice(section.ContentType, "paragraphs", "bullets", "entries") ?? "bullets";
|
||||
section.PresetKey = string.IsNullOrWhiteSpace(section.PresetKey) ? null : section.PresetKey.Trim().ToLowerInvariant();
|
||||
section.Items ??= new();
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ public static class HumanLanguageCatalog
|
||||
private static readonly Regex WordRegex = new(@"\p{L}+", RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex LevelRegex = new(
|
||||
@"\b(native(?:\s+speaker)?|fluent|advanced|intermediate|beginner|basic|conversational|elementary|professional\s+working\s+proficiency|working\s+proficiency|limited\s+working\s+proficiency|full\s+professional\s+proficiency|a1|a2|b1|b2|c1|c2|a1\s*/\s*a2|a2\s*/\s*b1|b1\s*/\s*b2|b2\s*/\s*c1|c1\s*/\s*c2)\b",
|
||||
@"\b(native(?:\s+speaker)?|fluent|advanced|intermediate|beginner|basic|conversational|elementary|professional\s+working\s+proficiency|working\s+proficiency|limited\s+working\s+proficiency|full\s+professional\s+proficiency|morsmål|flytende|avansert|mellomnivå|nybegynner|grunnleggende|samtalenivå|profesjonelt\s+arbeidsnivå|a1\s*/\s*a2|a2\s*/\s*b1|b1\s*/\s*b2|b2\s*/\s*c1|c1\s*/\s*c2|a1|a2|b1|b2|c1|c2)\b",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
public static string? NormalizeLanguageName(string? raw)
|
||||
@@ -79,6 +79,14 @@ public static class HumanLanguageCatalog
|
||||
"working proficiency" => "Working proficiency",
|
||||
"limited working proficiency" => "Limited working proficiency",
|
||||
"full professional proficiency" => "Full professional proficiency",
|
||||
"morsmål" => "Native",
|
||||
"flytende" => "Fluent",
|
||||
"avansert" => "Advanced",
|
||||
"mellomnivå" => "Intermediate",
|
||||
"nybegynner" => "Beginner",
|
||||
"grunnleggende" => "Basic",
|
||||
"samtalenivå" => "Conversational",
|
||||
"profesjonelt arbeidsnivå" => "Professional working proficiency",
|
||||
_ when Regex.IsMatch(compact, @"^[ABC][12](?:\s*/\s*[ABC][12])?$", RegexOptions.IgnoreCase) => compact.ToUpperInvariant().Replace(" ", string.Empty),
|
||||
_ => compact,
|
||||
};
|
||||
@@ -163,6 +171,7 @@ public static class HumanLanguageCatalog
|
||||
// means. They must beat culture enumeration (see Override), because ICU carries "Norwegian
|
||||
// Bokmål"/"Norwegian Nynorsk" and "Chinese (Simplified/Traditional)" as their own cultures.
|
||||
Override("norsk", "Norwegian");
|
||||
Override("engelsk", "English");
|
||||
Override("bokmal", "Norwegian");
|
||||
Override("bokmål", "Norwegian");
|
||||
Override("nynorsk", "Norwegian");
|
||||
|
||||
Reference in New Issue
Block a user