mix ci alias: compile --warning-as-errors, format --check-formatted, credo, dialyzer, test. Credo configured with sensible defaults. Dialyzer ignore file for false positives (Stripe types, Mix tasks, ExUnit internals). Credo fixes: map_join, filter consolidation, nesting extraction, cond→if simplification. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
233 lines
6.7 KiB
Elixir
233 lines
6.7 KiB
Elixir
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
|