berrypod/lib/simpleshop_theme/theme/css_generator.ex

171 lines
4.2 KiB
Elixir
Raw Normal View History

defmodule SimpleshopTheme.Theme.CSSGenerator do
@moduledoc """
Generates CSS custom properties (Layer 2: Theme Tokens) from theme settings.
This module converts ThemeSettings into CSS variables that bridge the gap
between fixed primitives (Layer 1) and semantic aliases (Layer 3).
"""
alias SimpleshopTheme.Settings.ThemeSettings
@doc """
Generates CSS for theme settings.
Returns a string of CSS custom properties that can be injected into a <style> tag.
"""
def generate(%ThemeSettings{} = settings) do
"""
/* Theme Tokens - Layer 2 (dynamically generated) */
.preview-frame, .shop-root {
#{generate_accent(settings.accent_color)}
#{generate_secondary_colors(settings)}
#{generate_font_size(settings.font_size)}
#{generate_heading_weight(settings.heading_weight)}
#{generate_layout_width(settings.layout_width)}
#{generate_button_style(settings.button_style)}
#{generate_product_text_align(settings.product_text_align)}
#{generate_image_aspect_ratio(settings.image_aspect_ratio)}
}
"""
|> String.trim()
end
# Accent color with HSL breakdown
defp generate_accent(hex_color) do
{h, s, l} = hex_to_hsl(hex_color)
"""
--t-accent-h: #{h};
--t-accent-s: #{s}%;
--t-accent-l: #{l}%;
"""
end
# Secondary colors
defp generate_secondary_colors(settings) do
"""
--t-secondary-accent: #{settings.secondary_accent_color};
--t-sale-color: #{settings.sale_color};
"""
end
# Font size variations
# Using 18px as base for better accessibility (WCAG recommends 18px+)
# Small: 18px, Medium: 19px, Large: 20px
defp generate_font_size("small") do
"--t-font-size-scale: 1.125;"
end
defp generate_font_size("medium") do
"--t-font-size-scale: 1.1875;"
end
defp generate_font_size("large") do
"--t-font-size-scale: 1.25;"
end
# Heading weight (override typography default)
defp generate_heading_weight("regular") do
"--t-heading-weight-override: 400;"
end
defp generate_heading_weight("medium") do
"--t-heading-weight-override: 500;"
end
defp generate_heading_weight("bold") do
"--t-heading-weight-override: 700;"
end
# Layout width
defp generate_layout_width("contained") do
"--t-layout-max-width: 1100px;"
end
defp generate_layout_width("wide") do
"--t-layout-max-width: 1400px;"
end
defp generate_layout_width("full") do
"--t-layout-max-width: 100%;"
end
# Button style
defp generate_button_style("filled") do
"--t-button-style: filled;"
end
defp generate_button_style("outline") do
"--t-button-style: outline;"
end
defp generate_button_style("soft") do
"--t-button-style: soft;"
end
# Product text alignment
defp generate_product_text_align("left") do
"--t-product-text-align: left;"
end
defp generate_product_text_align("center") do
"--t-product-text-align: center;"
end
# Image aspect ratio
defp generate_image_aspect_ratio("square") do
"--t-image-aspect-ratio: 1 / 1;"
end
defp generate_image_aspect_ratio("portrait") do
"--t-image-aspect-ratio: 3 / 4;"
end
defp generate_image_aspect_ratio("landscape") do
"--t-image-aspect-ratio: 4 / 3;"
end
# Convert hex color to HSL
defp hex_to_hsl("#" <> hex), do: hex_to_hsl(hex)
defp hex_to_hsl(hex) when byte_size(hex) == 6 do
{r, ""} = Integer.parse(String.slice(hex, 0..1), 16)
{g, ""} = Integer.parse(String.slice(hex, 2..3), 16)
{b, ""} = Integer.parse(String.slice(hex, 4..5), 16)
# Normalize RGB values to 0-1
r = r / 255
g = g / 255
b = b / 255
max = Enum.max([r, g, b])
min = Enum.min([r, g, b])
delta = max - min
# Calculate lightness
l = (max + min) / 2
# Calculate saturation and hue
{h, s} =
if delta == 0 do
{0, 0}
else
s = if l > 0.5, do: delta / (2 - max - min), else: delta / (max + min)
h =
cond do
max == r -> (g - b) / delta + if(g < b, do: 6, else: 0)
max == g -> (b - r) / delta + 2
max == b -> (r - g) / delta + 4
end
{h * 60, s}
end
{round(h), round(s * 100), round(l * 100)}
end
# Handle invalid hex values
defp hex_to_hsl(_), do: {0, 0, 50}
end