handbook_engine / app /core /theme.py
internationalscholarsprogram's picture
fix: ISP handbook styling overhaul - margins, typography, emphasis, benefits, CSS cascade
ec94fc1
"""Centralized handbook visual theme β€” single source of truth.
All colour values, font sizes, spacing, and rendering tokens live here.
Templates, CSS generation, and renderers reference this module instead
of hardcoding visual rules.
Spec source: ISP Handbook Enhancement Guidelines + sample PDF.
"""
from __future__ import annotations
from dataclasses import dataclass, field
# ── Colour palette ──────────────────────────────────────────────
@dataclass(frozen=True)
class Colors:
"""Every colour used in the handbook, named by purpose."""
heading_blue: str = "#1C75BC"
heading_green: str = "#1A9970"
body_text: str = "#000000"
toc_text: str = "#111111"
note_red: str = "#C00000"
link_blue: str = "#1C75BC"
benefits_header_bg: str = "#00F600"
benefits_header_fg: str = "#FFFFFF"
benefit_item_bg: str = "#00FCFC"
benefit_item_fg: str = "#000000"
school_info_green: str = "#1A9970"
table_border: str = "#333333"
table_header_bg: str = "#E6E6E6"
table_header_fg: str = "#333333"
toc_dots: str = "#777777"
muted: str = "#666666"
note_bg: str = "#F7F8FA"
note_border: str = "#BBBBBB"
page_bg: str = "#FFFFFF"
# ── Typography ──────────────────────────────────────────────────
@dataclass(frozen=True)
class Typography:
"""Font families, sizes, weights, and line-heights."""
font_family: str = "'Century Gothic', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif"
font_size_body: str = "10pt"
font_size_h1: str = "12pt"
font_size_h2: str = "12pt"
font_size_h3: str = "10pt"
font_size_toc_heading: str = "12pt"
font_size_toc_item: str = "10pt"
font_size_table: str = "9.5px"
font_size_programs_table: str = "8.5px"
font_size_career_list: str = "8.5px"
font_size_note: str = "9.5px"
font_size_benefits_header: str = "10.5px"
font_size_benefit_item: str = "10px"
font_size_school_name: str = "12pt"
font_size_summary_label: str = "10.5px"
font_size_summary_value: str = "9.5px"
font_size_qualify: str = "10px"
line_height_body: str = "1.4"
line_height_heading: str = "1.2"
line_height_table: str = "1.25"
# ── Spacing / margins ──────────────────────────────────────────
@dataclass(frozen=True)
class Spacing:
"""Page geometry and element margins. All margins: 2.54cm."""
page_margin_top: str = "2.54cm"
page_margin_right: str = "2.54cm"
page_margin_bottom: str = "2.54cm"
page_margin_left: str = "2.54cm"
paragraph_margin: str = "2px 0 8px"
heading_margin_h1: str = "12px 0 6px"
heading_margin_h2: str = "10px 0 4px"
list_margin: str = "2px 0 8px 18px"
note_padding: str = "6px 8px"
note_margin: str = "6px 0 8px"
table_margin: str = "6px 0 10px"
table_cell_padding: str = "5px 6px"
benefits_margin: str = "4px 0 4px"
school_top_summary_width: str = "58%"
school_top_campus_width: str = "42%"
# ── Table column widths ────────────────────────────────────────
@dataclass(frozen=True)
class ProgramTableColumns:
"""Fixed widths for the 5-column programs table."""
program: str = "22%"
designation: str = "14%"
entrance_exam: str = "12%"
career_pathways: str = "34%"
funding: str = "18%"
# ── Bullet characters ──────────────────────────────────────────
@dataclass(frozen=True)
class Bullets:
"""Bullet characters used throughout the handbook."""
primary: str = "\u27A2" # ➒
benefit: str = "\u2022" # β€’
career: str = "disc" # CSS list-style-type for career lists
# ── Render-block type registry ──────────────────────────────────
BLOCK_TYPES = (
"heading_1",
"heading_2",
"paragraph",
"bullet_list",
"note",
"table",
"enrollment_steps",
"school_profile",
"university_summary",
"toc",
"cover",
"full_page_image",
)
# ── Composed theme object ──────────────────────────────────────
@dataclass(frozen=True)
class HandbookTheme:
"""Complete handbook theme β€” inject into renderers and templates."""
colors: Colors = field(default_factory=Colors)
typography: Typography = field(default_factory=Typography)
spacing: Spacing = field(default_factory=Spacing)
program_columns: ProgramTableColumns = field(default_factory=ProgramTableColumns)
bullets: Bullets = field(default_factory=Bullets)
def css_vars(self) -> dict[str, str]:
"""Flatten theme to CSS custom properties (--hb-*)."""
v: dict[str, str] = {}
# Colors
for fname in Colors.__dataclass_fields__:
v[f"--hb-{fname.replace('_', '-')}"] = getattr(self.colors, fname)
# Typography
v["--hb-font-family"] = self.typography.font_family
v["--hb-font-size-body"] = self.typography.font_size_body
v["--hb-font-size-h1"] = self.typography.font_size_h1
v["--hb-font-size-h2"] = self.typography.font_size_h2
v["--hb-line-height-body"] = self.typography.line_height_body
# Spacing
v["--hb-page-margin-top"] = self.spacing.page_margin_top
v["--hb-page-margin-right"] = self.spacing.page_margin_right
v["--hb-page-margin-bottom"] = self.spacing.page_margin_bottom
v["--hb-page-margin-left"] = self.spacing.page_margin_left
# Bullet
v["--hb-bullet-char"] = f'"{self.bullets.primary}"'
return v
# Module-level singleton
THEME = HandbookTheme()