"""External UI theme loader for the warehouse desktop application. The module reads ``ui_theme.json`` from the workspace root and exposes a few helpers to resolve fonts, colors, paddings and section-specific configuration without hardcoding presentation details inside each window module. """ from __future__ import annotations import json from functools import lru_cache from pathlib import Path from typing import Any THEME_PATH = Path(__file__).with_name("ui_theme.json") @lru_cache(maxsize=1) def load_theme() -> dict[str, Any]: """Load the external JSON theme, returning an empty dict on failure.""" try: return json.loads(THEME_PATH.read_text(encoding="utf-8")) except Exception: return {} def reload_theme() -> dict[str, Any]: """Clear cache and reload the theme from disk.""" load_theme.cache_clear() return load_theme() def theme_section(name: str, default: dict[str, Any] | None = None) -> dict[str, Any]: """Return one top-level theme section as a dictionary.""" theme = load_theme() value = theme.get(name) if isinstance(value, dict): return value return dict(default or {}) def theme_value(section: dict[str, Any], key: str, default: Any = None) -> Any: """Return a scalar theme value from a section.""" return section.get(key, default) def theme_color(section: dict[str, Any], key: str, default: Any = None) -> Any: """Return a CTk-compatible color value (string or light/dark tuple).""" value = section.get(key, default) if isinstance(value, list) and len(value) == 2: return tuple(value) return value def theme_padding(section: dict[str, Any], key: str, default: tuple[int, ...]) -> tuple[int, ...]: """Return tuple-like padding values from a JSON list.""" value = section.get(key) if isinstance(value, list): try: return tuple(int(v) for v in value) except Exception: return default return default def theme_font(section: dict[str, Any], key: str, default: tuple[Any, ...]) -> tuple[Any, ...]: """Resolve a Tk font tuple from JSON.""" spec = section.get(key) if not isinstance(spec, dict): return default family = spec.get("family", default[0] if default else "Segoe UI") size = int(spec.get("size", default[1] if len(default) > 1 else 10)) parts: list[Any] = [family, size] weight = str(spec.get("weight", "")).strip().lower() if weight in {"bold", "normal"} and weight != "normal": parts.append(weight) slant = str(spec.get("slant", "")).strip().lower() if slant == "italic": parts.append(slant) if bool(spec.get("underline", False)): parts.append("underline") if bool(spec.get("overstrike", False)): parts.append("overstrike") return tuple(parts)