defmodule SimpleshopTheme.Theme.Fonts do @moduledoc """ Centralized font configuration for the theme system. This module defines all available fonts and their variants, maps typography presets to font pairs, and generates CSS @font-face declarations. Font files are expected to be in /priv/static/fonts/ and served at /fonts/. """ # Font definitions: family name, file prefix, and available weights @fonts %{ inter: %{ family: "Inter", file_prefix: "inter-v20-latin", weights: [300, 400, 500, 600, 700], fallback: "system-ui, sans-serif" }, manrope: %{ family: "Manrope", file_prefix: "manrope-v20-latin", weights: [400, 500, 600, 700], fallback: "system-ui, sans-serif" }, raleway: %{ family: "Raleway", file_prefix: "raleway-v37-latin", weights: [300, 400, 500], fallback: "system-ui, sans-serif" }, playfair: %{ family: "Playfair Display", file_prefix: "playfair-display-v40-latin", weights: [400, 500, 700], fallback: "Georgia, serif" }, space_grotesk: %{ family: "Space Grotesk", file_prefix: "space-grotesk-v22-latin", weights: [400, 500, 600], fallback: "system-ui, sans-serif" }, cormorant: %{ family: "Cormorant Garamond", file_prefix: "cormorant-garamond-v21-latin", weights: [400, 500, 600], fallback: "Georgia, serif" }, source_serif: %{ family: "Source Serif 4", file_prefix: "source-serif-4-v14-latin", weights: [400, 600], fallback: "Georgia, serif" }, fraunces: %{ family: "Fraunces", file_prefix: "fraunces-v38-latin", weights: [400, 500, 600, 700], fallback: "Georgia, serif" }, work_sans: %{ family: "Work Sans", file_prefix: "work-sans-v24-latin", weights: [300, 400, 500, 600], fallback: "system-ui, sans-serif" }, dm_sans: %{ family: "DM Sans", file_prefix: "dm-sans-v17-latin", weights: [400, 500, 600, 700], fallback: "system-ui, sans-serif" } } # Typography presets map to heading and body font keys @typography_fonts %{ "clean" => %{heading: :manrope, body: :inter}, "editorial" => %{heading: :playfair, body: :raleway}, "modern" => %{heading: :space_grotesk, body: :inter}, "classic" => %{heading: :cormorant, body: :source_serif}, "friendly" => %{heading: :fraunces, body: :work_sans}, "minimal" => %{heading: :dm_sans, body: :source_serif}, "impulse" => %{heading: :raleway, body: :inter} } @doc """ Returns the font configuration for a given font key. """ def get_font(key), do: Map.get(@fonts, key) @doc """ Returns all font configurations. """ def all_fonts, do: @fonts @doc """ Returns the heading and body font keys for a typography preset. """ def fonts_for_typography(typography) do Map.get(@typography_fonts, typography, @typography_fonts["clean"]) end @doc """ Returns the CSS font-family declaration (with fallbacks) for a font key. """ def font_family(key) do case get_font(key) do %{family: family, fallback: fallback} -> "'#{family}', #{fallback}" nil -> "system-ui, sans-serif" end end @doc """ Generates @font-face CSS for a specific typography preset. Only includes the fonts needed for that preset. Accepts an optional path_resolver function to transform font URLs. In production, pass `&SimpleshopThemeWeb.Endpoint.static_path/1` for digested paths. """ def generate_font_faces(typography, path_resolver \\ &default_path_resolver/1) do %{heading: heading_key, body: body_key} = fonts_for_typography(typography) font_keys = if heading_key == body_key do [heading_key] else [heading_key, body_key] end Enum.map_join(font_keys, "\n", &generate_font_face_for_font(&1, path_resolver)) end @doc """ Generates @font-face CSS for ALL fonts. Used in the theme editor where users can switch between typography presets. Accepts an optional path_resolver function for digested paths. """ def generate_all_font_faces(path_resolver \\ &default_path_resolver/1) do @fonts |> Map.keys() |> Enum.map_join("\n", &generate_font_face_for_font(&1, path_resolver)) end @doc """ Returns the font file paths (without /fonts prefix) for a typography preset. Used by templates to generate preload links with digested paths via ~p sigil. """ def preload_font_paths(typography) do %{heading: heading_key, body: body_key} = fonts_for_typography(typography) # Preload the most commonly used weights preload_weights = %{ # For headings, preload the typical heading weight (500-600) heading: [500, 600], # For body, preload regular and semibold body: [400, 600] } heading_paths = font_paths_for_weights(heading_key, preload_weights.heading) body_paths = font_paths_for_weights(body_key, preload_weights.body) # Deduplicate in case heading and body use the same font (heading_paths ++ body_paths) |> Enum.uniq() end defp font_paths_for_weights(key, weights) do case get_font(key) do %{file_prefix: prefix, weights: available_weights} -> weights |> Enum.filter(&(&1 in available_weights)) |> Enum.map(fn weight -> weight_suffix = if weight == 400, do: "regular", else: to_string(weight) "#{prefix}-#{weight_suffix}.woff2" end) nil -> [] end end @doc """ Generates preload link tags for a specific typography preset. Returns a list of maps with href, as, type, and crossorigin attributes. Accepts an optional path_resolver function for digested paths. In production, pass `&SimpleshopThemeWeb.Endpoint.static_path/1`. """ def preload_links(typography, path_resolver \\ &default_path_resolver/1) do typography |> preload_font_paths() |> Enum.map(fn filename -> %{ href: path_resolver.("/fonts/#{filename}"), as: "font", type: "font/woff2", crossorigin: true } end) end # Private functions defp default_path_resolver(path), do: path defp generate_font_face_for_font(key, path_resolver) do case get_font(key) do %{family: family, file_prefix: prefix, weights: weights} -> Enum.map_join(weights, "", fn weight -> weight_suffix = if weight == 400, do: "regular", else: to_string(weight) font_path = path_resolver.("/fonts/#{prefix}-#{weight_suffix}.woff2") """ @font-face { font-family: '#{family}'; font-style: normal; font-weight: #{weight}; font-display: swap; src: url('#{font_path}') format('woff2'); } """ end) nil -> "" end end end