Files
jobtrackingapp/JobTrackerApi/Models/CvTheme.cs
T
cesnimda ce76046a29 feat: complete release readiness work
- consolidate API ownership and remove dead vendor code

- add Stripe billing, learning paths, and public CV hardening

- add migration, recovery, security, audit, and browser gates
2026-07-31 16:54:16 +02:00

154 lines
7.9 KiB
C#

namespace JobTrackerApi.Models;
// A theme is DATA, not code. The renderer (ThemedCvRenderer) has one render path; every visual
// difference between themes is expressed by the fields below. Adding a theme = adding one CvTheme to
// CvThemeCatalog — no renderer changes. This is the core Phase 4 architectural rule
// ("Do NOT hardcode HTML templates"). docs/architecture/cv-theme-engine.md.
public sealed class CvTheme
{
public string Id { get; init; } = string.Empty;
public string Name { get; init; } = string.Empty;
public string Category { get; init; } = string.Empty;
public string Description { get; init; } = string.Empty;
// single | sidebar-left | sidebar-right | header-band
public string Layout { get; init; } = "single";
public double SidebarWidthMm { get; init; } = 62;
// Palette
public string Accent { get; init; } = "#2563eb";
public string Ink { get; init; } = "#111827";
public string Muted { get; init; } = "#4b5563";
public string Line { get; init; } = "#d1d5db";
public string Paper { get; init; } = "#ffffff";
public string SidebarBg { get; init; } = "#f3f4f6";
public string SidebarInk { get; init; } = "#111827";
public string? HeadingColor { get; init; } // null => accent
// Typography
public string HeadingFont { get; init; } = "Georgia, 'Times New Roman', serif";
public string BodyFont { get; init; } = "Arial, Helvetica, sans-serif";
public double NameSizePt { get; init; } = 24;
public double HeadingSizePt { get; init; } = 12;
public double BodySizePt { get; init; } = 10;
public double LineHeight { get; init; } = 1.42;
// Spacing (mm)
public double PageMarginMm { get; init; } = 16;
public double SectionGapMm { get; init; } = 6;
public double EntryGapMm { get; init; } = 4.5;
// plain | band | centered | kicker
public string HeaderStyle { get; init; } = "plain";
// caps-rule | underline | plain | bar
public string HeadingStyle { get; init; } = "caps-rule";
// none | square | rounded | circle
public string PhotoShape { get; init; } = "rounded";
public bool DefaultIcons { get; init; }
// ATS-friendly = single-column, no sidebar tables/graphics that trip naive resume parsers. Set on
// 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; }
// 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" };
}
public static class CvThemeCatalog
{
// 8 professional themes, all data. To add one: append here.
public static readonly IReadOnlyList<CvTheme> Themes = new List<CvTheme>
{
new()
{
Id = "modern", Name = "Modern", Category = "Modern Professional",
Description = "Clean SaaS-style layout with a confident accent header.",
Layout = "header-band", HeaderStyle = "band", HeadingStyle = "caps-rule",
Accent = "#2563eb", HeadingFont = "'Segoe UI', Roboto, Arial, sans-serif", BodyFont = "'Segoe UI', Roboto, Arial, sans-serif",
PhotoShape = "rounded", DefaultIcons = true, AtsFriendly = true,
},
new()
{
Id = "minimal", Name = "Minimal", Category = "Minimal",
Description = "Quiet, spacious, typography-first. Nothing competes with the content.",
Layout = "single", HeaderStyle = "plain", HeadingStyle = "plain",
Accent = "#111827", HeadingColor = "#111827", Muted = "#6b7280",
HeadingFont = "'Helvetica Neue', Arial, sans-serif", BodyFont = "'Helvetica Neue', Arial, sans-serif",
SectionGapMm = 7, EntryGapMm = 5, PhotoShape = "none", AtsFriendly = true,
},
new()
{
Premium = true, Id = "executive", Name = "Executive", Category = "Executive",
Description = "High-contrast, serif, leadership-weighted. For senior and client-facing roles.",
Layout = "single", HeaderStyle = "centered", HeadingStyle = "underline",
Accent = "#7c2d12", Ink = "#1c1917", Muted = "#57534e", Line = "#1c1917",
HeadingFont = "Georgia, 'Times New Roman', serif", BodyFont = "Georgia, 'Times New Roman', serif",
NameSizePt = 27, PhotoShape = "square", AtsFriendly = true,
},
new()
{
Premium = true, Id = "technical", Name = "Technical", Category = "Technical",
Description = "Dense two-column layout tuned for engineering CVs — skills and projects up front.",
Layout = "sidebar-left", HeaderStyle = "band", HeadingStyle = "bar",
Accent = "#0f4c5c", Ink = "#102a43", Muted = "#486581", SidebarBg = "#0f4c5c", SidebarInk = "#ffffff",
HeadingFont = "'Roboto', Arial, sans-serif", BodyFont = "'Roboto', Arial, sans-serif",
BodySizePt = 9.5, LineHeight = 1.36, DefaultIcons = true,
SidebarSections = new() { "contact", "skills", "languages", "certifications" },
},
new()
{
Id = "ats-classic", Name = "ATS Classic", Category = "ATS Professional",
Description = "Single column, no graphics, maximum parser compatibility. The safe default.",
Layout = "single", HeaderStyle = "plain", HeadingStyle = "caps-rule",
Accent = "#334155", Ink = "#111827", Muted = "#374151",
HeadingFont = "Arial, Helvetica, sans-serif", BodyFont = "Arial, Helvetica, sans-serif",
PhotoShape = "none", AtsFriendly = true,
},
new()
{
Premium = true, Id = "nordic", Name = "Nordic", Category = "Modern Professional",
Description = "Calm cool-blue sidebar, generous whitespace, Scandinavian restraint.",
Layout = "sidebar-right", HeaderStyle = "plain", HeadingStyle = "caps-rule",
Accent = "#3b6ea5", Ink = "#1f2937", Muted = "#4b5563", SidebarBg = "#eef3f8", SidebarInk = "#1f2937",
HeadingFont = "'Segoe UI', Arial, sans-serif", BodyFont = "'Segoe UI', Arial, sans-serif",
PhotoShape = "circle", DefaultIcons = true,
},
new()
{
Premium = true, Id = "elegant", Name = "Elegant", Category = "Creative",
Description = "Editorial serif headings over sans body, premium spacing and a plum accent.",
Layout = "single", HeaderStyle = "kicker", HeadingStyle = "underline",
Accent = "#7c3aed", Ink = "#1f2937", Muted = "#4b5563",
HeadingFont = "Georgia, 'Times New Roman', serif", BodyFont = "'Segoe UI', Arial, sans-serif",
NameSizePt = 26, SectionGapMm = 7, PhotoShape = "circle", AtsFriendly = true,
},
new()
{
Premium = true, Id = "creative", Name = "Creative", Category = "Creative",
Description = "Bold accent sidebar and photo-forward header for design and product roles.",
Layout = "sidebar-left", HeaderStyle = "band", HeadingStyle = "bar",
Accent = "#db2777", Ink = "#18181b", Muted = "#52525b", SidebarBg = "#be185d", SidebarInk = "#ffffff",
HeadingFont = "'Poppins', 'Segoe UI', Arial, sans-serif", BodyFont = "'Segoe UI', Arial, sans-serif",
PhotoShape = "circle", DefaultIcons = true,
SidebarSections = new() { "contact", "skills", "languages", "interests" },
},
};
public static CvTheme Resolve(string? id)
{
var key = (id ?? string.Empty).Trim().ToLowerInvariant();
return Themes.FirstOrDefault(t => t.Id == key) ?? Themes[0];
}
public static bool CanUse(string? id, bool premiumThemes) =>
Exists(id) && (premiumThemes || !Resolve(id).Premium);
public static bool Exists(string? id)
{
var key = (id ?? string.Empty).Trim().ToLowerInvariant();
return Themes.Any(t => t.Id == key);
}
}