migrate accent colours from HSL to oklch, inject theme into admin
Phase 1: Replace hex_to_hsl with hex_to_oklch in CSSGenerator, output --t-accent-l/c/h instead of --t-accent-h/s/l. All 46 HSL accent references across theme-semantic.css, theme-layer2-attributes.css, and shop/components.css replaced with oklch/color-mix equivalents. Dead style*= attribute selectors for button variants replaced with proper class-based selectors. Added color-scheme: light/dark to mood output. Phase 2: Add LoadTheme plug to admin pipeline, extend AdminLayoutHook with theme_settings and generated_css assigns, add font preloads and generated CSS injection to admin_root.html.heex. No visual changes to admin yet — .themed wrapper added in next phase. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -61,7 +61,8 @@ defmodule Berrypod.Theme.CSSGenerator do
|
||||
# Mood colors - surface, text, and border colors
|
||||
defp generate_mood("neutral") do
|
||||
"""
|
||||
--t-surface-base: #ffffff;
|
||||
color-scheme: light;
|
||||
--t-surface-base: #ffffff;
|
||||
--t-surface-raised: #ffffff;
|
||||
--t-surface-sunken: #f5f5f5;
|
||||
--t-surface-overlay: rgba(255, 255, 255, 0.95);
|
||||
@@ -76,7 +77,8 @@ defmodule Berrypod.Theme.CSSGenerator do
|
||||
|
||||
defp generate_mood("warm") do
|
||||
"""
|
||||
--t-surface-base: #fdf8f3;
|
||||
color-scheme: light;
|
||||
--t-surface-base: #fdf8f3;
|
||||
--t-surface-raised: #fffcf8;
|
||||
--t-surface-sunken: #f5ebe0;
|
||||
--t-surface-overlay: rgba(253, 248, 243, 0.95);
|
||||
@@ -91,7 +93,8 @@ defmodule Berrypod.Theme.CSSGenerator do
|
||||
|
||||
defp generate_mood("cool") do
|
||||
"""
|
||||
--t-surface-base: #f4f7fb;
|
||||
color-scheme: light;
|
||||
--t-surface-base: #f4f7fb;
|
||||
--t-surface-raised: #f8fafc;
|
||||
--t-surface-sunken: #e8eff7;
|
||||
--t-surface-overlay: rgba(244, 247, 251, 0.95);
|
||||
@@ -106,7 +109,8 @@ defmodule Berrypod.Theme.CSSGenerator do
|
||||
|
||||
defp generate_mood("dark") do
|
||||
"""
|
||||
--t-surface-base: #0a0a0a;
|
||||
color-scheme: dark;
|
||||
--t-surface-base: #0a0a0a;
|
||||
--t-surface-raised: #171717;
|
||||
--t-surface-sunken: #000000;
|
||||
--t-surface-overlay: rgba(23, 23, 23, 0.95);
|
||||
@@ -239,14 +243,14 @@ defmodule Berrypod.Theme.CSSGenerator do
|
||||
# Fallback for any other density value
|
||||
defp generate_density(_), do: generate_density("balanced")
|
||||
|
||||
# Accent color with HSL breakdown
|
||||
# Accent color with oklch breakdown
|
||||
defp generate_accent(hex_color) do
|
||||
{h, s, l} = hex_to_hsl(hex_color)
|
||||
{l, c, h} = hex_to_oklch(hex_color)
|
||||
|
||||
"""
|
||||
--t-accent-h: #{h};
|
||||
--t-accent-s: #{s}%;
|
||||
--t-accent-l: #{l}%;
|
||||
--t-accent-c: #{c};
|
||||
--t-accent-h: #{h};
|
||||
"""
|
||||
end
|
||||
|
||||
@@ -334,46 +338,55 @@ defmodule Berrypod.Theme.CSSGenerator do
|
||||
"--t-image-aspect-ratio: 4 / 3;"
|
||||
end
|
||||
|
||||
# Convert hex color to HSL
|
||||
defp hex_to_hsl("#" <> hex), do: hex_to_hsl(hex)
|
||||
# Convert hex colour to oklch (lightness%, chroma, hue°)
|
||||
# Pipeline: hex → sRGB → linear RGB → XYZ (D65) → oklab → oklch
|
||||
defp hex_to_oklch("#" <> hex), do: hex_to_oklch(hex)
|
||||
|
||||
defp hex_to_hsl(hex) when byte_size(hex) == 6 do
|
||||
defp hex_to_oklch(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
|
||||
# sRGB to linear RGB (inverse gamma)
|
||||
lr = srgb_to_linear(r / 255)
|
||||
lg = srgb_to_linear(g / 255)
|
||||
lb = srgb_to_linear(b / 255)
|
||||
|
||||
max = Enum.max([r, g, b])
|
||||
min = Enum.min([r, g, b])
|
||||
delta = max - min
|
||||
# Linear RGB to oklab via the Björn Ottosson method
|
||||
# First: linear sRGB → LMS (cube root of cone response)
|
||||
l_ = :math.pow(0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb, 1 / 3)
|
||||
m_ = :math.pow(0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb, 1 / 3)
|
||||
s_ = :math.pow(0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb, 1 / 3)
|
||||
|
||||
# Calculate lightness
|
||||
l = (max + min) / 2
|
||||
# LMS to oklab
|
||||
ok_l = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_
|
||||
ok_a = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_
|
||||
ok_b = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_
|
||||
|
||||
# Calculate saturation and hue
|
||||
{h, s} =
|
||||
if delta == 0 do
|
||||
{0, 0}
|
||||
# oklab to oklch
|
||||
c = :math.sqrt(ok_a * ok_a + ok_b * ok_b)
|
||||
|
||||
h =
|
||||
if c < 0.0001 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}
|
||||
h_rad = :math.atan2(ok_b, ok_a)
|
||||
h_deg = h_rad * 180 / :math.pi()
|
||||
if h_deg < 0, do: h_deg + 360, else: h_deg
|
||||
end
|
||||
|
||||
{round(h), round(s * 100), round(l * 100)}
|
||||
# Return as {lightness%, chroma, hue°} rounded for CSS
|
||||
{
|
||||
Float.round(ok_l * 100, 2),
|
||||
Float.round(c, 4),
|
||||
Float.round(h, 2)
|
||||
}
|
||||
end
|
||||
|
||||
# Handle invalid hex values
|
||||
defp hex_to_hsl(_), do: {0, 0, 50}
|
||||
# Handle invalid hex values (neutral grey fallback)
|
||||
defp hex_to_oklch(_), do: {50.0, 0.0, 0.0}
|
||||
|
||||
# sRGB gamma decompression (IEC 61966-2-1)
|
||||
defp srgb_to_linear(v) when v <= 0.04045, do: v / 12.92
|
||||
defp srgb_to_linear(v), do: :math.pow((v + 0.055) / 1.055, 2.4)
|
||||
end
|
||||
|
||||
@@ -1,21 +1,40 @@
|
||||
defmodule BerrypodWeb.AdminLayoutHook do
|
||||
@moduledoc """
|
||||
LiveView on_mount hook that assigns the current path for admin sidebar navigation.
|
||||
LiveView on_mount hook that assigns the current path for admin sidebar navigation
|
||||
and loads theme settings for the admin layout.
|
||||
"""
|
||||
import Phoenix.Component
|
||||
|
||||
alias Berrypod.Settings
|
||||
alias Berrypod.Theme.{CSSCache, CSSGenerator}
|
||||
|
||||
def on_mount(:assign_current_path, _params, _session, socket) do
|
||||
theme_settings = Settings.get_theme_settings()
|
||||
|
||||
generated_css =
|
||||
case CSSCache.get() do
|
||||
{:ok, css} ->
|
||||
css
|
||||
|
||||
:miss ->
|
||||
css = CSSGenerator.generate(theme_settings, &BerrypodWeb.Endpoint.static_path/1)
|
||||
CSSCache.put(css)
|
||||
css
|
||||
end
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(:current_path, "")
|
||||
|> assign(:site_live, Berrypod.Settings.site_live?())
|
||||
|> assign(:site_live, Settings.site_live?())
|
||||
|> assign(:theme_settings, theme_settings)
|
||||
|> assign(:generated_css, generated_css)
|
||||
|> Phoenix.LiveView.attach_hook(:set_current_path, :handle_params, fn _params,
|
||||
uri,
|
||||
socket ->
|
||||
{:cont,
|
||||
socket
|
||||
|> assign(:current_path, URI.parse(uri).path)
|
||||
|> assign(:site_live, Berrypod.Settings.site_live?())}
|
||||
|> assign(:site_live, Settings.site_live?())}
|
||||
end)
|
||||
|
||||
{:cont, socket}
|
||||
|
||||
@@ -7,12 +7,23 @@
|
||||
<.live_title default="Admin" suffix=" · Berrypod">
|
||||
{assigns[:page_title]}
|
||||
</.live_title>
|
||||
<!-- Preload critical fonts for the current typography preset -->
|
||||
<%= for preload <- Berrypod.Theme.Fonts.preload_links(
|
||||
@theme_settings.typography,
|
||||
&BerrypodWeb.Endpoint.static_path/1
|
||||
) do %>
|
||||
<link rel="preload" href={preload.href} as="font" type="font/woff2" crossorigin />
|
||||
<% end %>
|
||||
<!-- Pre-declare layer order so shop reset < Tailwind base regardless of load order -->
|
||||
<style>
|
||||
@layer properties, reset, primitives, tokens, theme, base, components, layout, utilities, overrides;
|
||||
</style>
|
||||
<link phx-track-static rel="stylesheet" href={~p"/assets/css/admin.css"} />
|
||||
<link phx-track-static rel="stylesheet" href={~p"/assets/css/shop.css"} />
|
||||
<!-- Generated theme CSS with @font-face declarations -->
|
||||
<style id="theme-css">
|
||||
<%= Phoenix.HTML.raw(@generated_css) %>
|
||||
</style>
|
||||
<script defer phx-track-static src={~p"/assets/js/app.js"}>
|
||||
</script>
|
||||
<script>
|
||||
|
||||
@@ -40,6 +40,7 @@ defmodule BerrypodWeb.Router do
|
||||
|
||||
pipeline :admin do
|
||||
plug :put_root_layout, html: {BerrypodWeb.Layouts, :admin_root}
|
||||
plug BerrypodWeb.Plugs.LoadTheme
|
||||
end
|
||||
|
||||
# Public storefront (root level)
|
||||
|
||||
Reference in New Issue
Block a user