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:
parent
eb65b11e4d
commit
285aafa0b5
@ -131,7 +131,7 @@
|
||||
/* ── Product prices (shared between cards and PDP) ── */
|
||||
|
||||
.product-price--sale {
|
||||
color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
|
||||
color: var(--t-accent);
|
||||
}
|
||||
|
||||
.product-price--compare {
|
||||
@ -254,7 +254,7 @@
|
||||
.hero-pre-title {
|
||||
font-family: var(--t-font-heading);
|
||||
font-weight: var(--t-heading-weight);
|
||||
color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
|
||||
color: var(--t-accent);
|
||||
font-size: var(--t-heading-display);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@ -398,7 +398,7 @@
|
||||
}
|
||||
|
||||
.accent-link {
|
||||
color: var(--t-accent-text, hsl(var(--t-accent-h) var(--t-accent-s) 38%));
|
||||
color: var(--t-accent-text);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
font-size: var(--t-text-small);
|
||||
@ -457,7 +457,7 @@
|
||||
.collection-empty-link {
|
||||
display: inline-block;
|
||||
margin-top: 1rem;
|
||||
color: var(--t-text-accent, hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)));
|
||||
color: var(--t-text-accent, var(--t-accent));
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@ -518,7 +518,7 @@
|
||||
}
|
||||
|
||||
.pdp-thumbnail-active {
|
||||
border: 2px solid hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
|
||||
border: 2px solid var(--t-accent);
|
||||
}
|
||||
|
||||
/* ── Variant selector ── */
|
||||
@ -556,8 +556,8 @@
|
||||
padding: 0;
|
||||
|
||||
&[aria-pressed="true"] {
|
||||
border-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
|
||||
--tw-ring-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
|
||||
border-color: var(--t-accent);
|
||||
--tw-ring-color: var(--t-accent);
|
||||
}
|
||||
}
|
||||
|
||||
@ -572,8 +572,8 @@
|
||||
cursor: pointer;
|
||||
|
||||
&[aria-pressed="true"] {
|
||||
border-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
|
||||
background: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l) / 0.1);
|
||||
border-color: var(--t-accent);
|
||||
background: color-mix(in oklch, var(--t-accent) 10%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
@ -654,7 +654,7 @@
|
||||
}
|
||||
|
||||
.atc-btn {
|
||||
background-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
|
||||
background-color: var(--t-accent);
|
||||
color: var(--t-text-inverse);
|
||||
border-radius: var(--t-radius-button);
|
||||
border: none;
|
||||
@ -762,7 +762,7 @@
|
||||
/* ── Announcement bar ── */
|
||||
|
||||
.announcement-bar {
|
||||
background-color: var(--t-accent-button, hsl(var(--t-accent-h) var(--t-accent-s) 42%));
|
||||
background-color: var(--t-accent-button);
|
||||
color: var(--t-text-inverse);
|
||||
text-align: center;
|
||||
padding: 0.5rem 1rem;
|
||||
@ -867,7 +867,7 @@
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
background: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
|
||||
background: var(--t-accent);
|
||||
color: var(--t-text-inverse);
|
||||
font-size: var(--t-text-caption);
|
||||
font-weight: 600;
|
||||
@ -889,7 +889,7 @@
|
||||
|
||||
&[aria-current="page"] {
|
||||
color: var(--t-text-primary);
|
||||
border-bottom-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
|
||||
border-bottom-color: var(--t-accent);
|
||||
}
|
||||
}
|
||||
|
||||
@ -943,9 +943,9 @@
|
||||
}
|
||||
|
||||
&[aria-current="page"] {
|
||||
color: hsl(var(--t-accent-h) var(--t-accent-s) calc(var(--t-accent-l) - 15%));
|
||||
color: color-mix(in oklch, var(--t-accent) 80%, black);
|
||||
font-weight: 600;
|
||||
background-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l) / 0.1);
|
||||
background-color: color-mix(in oklch, var(--t-accent) 10%, transparent);
|
||||
|
||||
& svg {
|
||||
width: 1.5rem;
|
||||
@ -1273,7 +1273,7 @@
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
background: var(--t-accent-button, hsl(var(--t-accent-h) var(--t-accent-s) 42%));
|
||||
background: var(--t-accent-button);
|
||||
color: var(--t-text-inverse);
|
||||
border-radius: var(--t-radius-button);
|
||||
border: none;
|
||||
@ -1692,7 +1692,7 @@
|
||||
/* ── Accent email link ── */
|
||||
|
||||
.accent-email {
|
||||
color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
|
||||
color: var(--t-accent);
|
||||
}
|
||||
|
||||
/* ── Card shared styles (info, tracking, newsletter, social cards) ── */
|
||||
@ -1767,7 +1767,7 @@
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
|
||||
color: var(--t-accent);
|
||||
text-decoration: none;
|
||||
|
||||
& svg {
|
||||
@ -1879,7 +1879,7 @@
|
||||
.trust-badge-icon {
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.125rem;
|
||||
color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
|
||||
color: var(--t-accent);
|
||||
|
||||
& svg {
|
||||
width: 1.25rem;
|
||||
|
||||
@ -157,19 +157,21 @@
|
||||
|
||||
/* Button Style Variants */
|
||||
&[data-button-style="outline"] {
|
||||
& button[style*="background-color: hsl(var(--t-accent"],
|
||||
& a[style*="background-color: hsl(var(--t-accent"] {
|
||||
& .themed-button,
|
||||
& .atc-btn,
|
||||
& .cart-drawer-checkout {
|
||||
background-color: transparent !important;
|
||||
color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)) !important;
|
||||
border: 2px solid hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)) !important;
|
||||
color: var(--t-accent) !important;
|
||||
border: 2px solid var(--t-accent) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-button-style="soft"] {
|
||||
& button[style*="background-color: hsl(var(--t-accent"],
|
||||
& a[style*="background-color: hsl(var(--t-accent"] {
|
||||
background-color: hsl(var(--t-accent-h) var(--t-accent-s) 90%) !important;
|
||||
color: hsl(var(--t-accent-h) var(--t-accent-s) 30%) !important;
|
||||
& .themed-button,
|
||||
& .atc-btn,
|
||||
& .cart-drawer-checkout {
|
||||
background-color: color-mix(in oklch, var(--t-accent) 12%, var(--t-surface-base)) !important;
|
||||
color: color-mix(in oklch, var(--t-accent) 80%, black) !important;
|
||||
border: 2px solid transparent !important;
|
||||
}
|
||||
}
|
||||
@ -231,12 +233,12 @@
|
||||
}
|
||||
|
||||
& .filter-pill-active {
|
||||
background-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
|
||||
background-color: var(--t-accent);
|
||||
color: var(--t-text-inverse);
|
||||
border-color: transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: hsl(var(--t-accent-h) var(--t-accent-s) calc(var(--t-accent-l) - 5%));
|
||||
background-color: var(--t-accent-hover);
|
||||
color: var(--t-text-inverse);
|
||||
}
|
||||
}
|
||||
@ -250,7 +252,7 @@
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
|
||||
border-color: var(--t-accent);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
@ -269,12 +271,12 @@
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
|
||||
border-color: var(--t-accent);
|
||||
}
|
||||
}
|
||||
|
||||
& .themed-button {
|
||||
background-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
|
||||
background-color: var(--t-accent);
|
||||
color: var(--t-text-inverse);
|
||||
border-radius: var(--t-radius-button);
|
||||
border: none;
|
||||
@ -322,7 +324,7 @@
|
||||
}
|
||||
|
||||
.badge-new {
|
||||
background-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
|
||||
background-color: var(--t-accent);
|
||||
color: var(--t-text-inverse);
|
||||
}
|
||||
|
||||
|
||||
@ -13,21 +13,19 @@
|
||||
color: var(--t-text-primary);
|
||||
font-family: var(--t-font-body);
|
||||
|
||||
/* Accent color - HSL components set dynamically by CSS generator */
|
||||
--t-accent-h: 24;
|
||||
--t-accent-s: 95%;
|
||||
--t-accent-l: 53%;
|
||||
/* Accent colour — oklch components set dynamically by CSSGenerator */
|
||||
--t-accent-l: 65%;
|
||||
--t-accent-c: 0.2;
|
||||
--t-accent-h: 47;
|
||||
|
||||
--t-accent: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
|
||||
--t-accent-hover: hsl(var(--t-accent-h) var(--t-accent-s) calc(var(--t-accent-l) - 8%));
|
||||
--t-accent-subtle: hsl(var(--t-accent-h) 40% 95%);
|
||||
--t-accent-ring: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l) / 0.4);
|
||||
--t-accent: oklch(var(--t-accent-l) var(--t-accent-c) var(--t-accent-h));
|
||||
--t-accent-hover: color-mix(in oklch, var(--t-accent) 85%, black);
|
||||
--t-accent-subtle: color-mix(in oklch, var(--t-accent) 8%, var(--t-surface-base));
|
||||
--t-accent-ring: oklch(var(--t-accent-l) var(--t-accent-c) var(--t-accent-h) / 0.4);
|
||||
|
||||
/* WCAG AA compliant accent variants for better contrast */
|
||||
/* Darker accent for text on light backgrounds (4.5:1 with white) */
|
||||
--t-accent-text: hsl(var(--t-accent-h) var(--t-accent-s) 38%);
|
||||
/* Darker accent for button backgrounds to ensure 4.5:1 with white text */
|
||||
--t-accent-button: hsl(var(--t-accent-h) var(--t-accent-s) 42%);
|
||||
--t-accent-text: color-mix(in oklch, var(--t-accent) 80%, black);
|
||||
--t-accent-button: color-mix(in oklch, var(--t-accent) 85%, black);
|
||||
|
||||
/* Secondary colors */
|
||||
--t-secondary-accent: #ea580c;
|
||||
@ -117,7 +115,7 @@
|
||||
|
||||
/* Dark mode accent-subtle override */
|
||||
&[data-mood="dark"] {
|
||||
--t-accent-subtle: hsl(var(--t-accent-h) 30% 15%);
|
||||
--t-accent-subtle: color-mix(in oklch, var(--t-accent) 15%, black);
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,7 +135,7 @@
|
||||
& textarea:focus-visible,
|
||||
& [tabindex]:focus-visible,
|
||||
& details summary:focus-visible {
|
||||
outline: 2px solid hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
|
||||
outline: 2px solid var(--t-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@ -230,9 +228,9 @@
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l) / 0.12);
|
||||
color: hsl(var(--t-accent-h) var(--t-accent-s) calc(var(--t-accent-l) - 15%));
|
||||
border-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l) / 0.25);
|
||||
background-color: color-mix(in oklch, var(--t-accent) 12%, transparent);
|
||||
color: color-mix(in oklch, var(--t-accent) 80%, black);
|
||||
border-color: color-mix(in oklch, var(--t-accent) 25%, transparent);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
@ -273,7 +271,7 @@
|
||||
top: -100px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
|
||||
background: var(--t-accent);
|
||||
color: var(--t-text-inverse);
|
||||
padding: 0.75rem 1.5rem;
|
||||
z-index: 9999;
|
||||
@ -312,5 +310,5 @@
|
||||
|
||||
/* Active nav underline — must stay unlayered to match the base border above */
|
||||
.shop-nav [aria-current="page"] {
|
||||
border-bottom-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
|
||||
border-bottom-color: var(--t-accent);
|
||||
}
|
||||
|
||||
@ -61,6 +61,7 @@ defmodule Berrypod.Theme.CSSGenerator do
|
||||
# Mood colors - surface, text, and border colors
|
||||
defp generate_mood("neutral") do
|
||||
"""
|
||||
color-scheme: light;
|
||||
--t-surface-base: #ffffff;
|
||||
--t-surface-raised: #ffffff;
|
||||
--t-surface-sunken: #f5f5f5;
|
||||
@ -76,6 +77,7 @@ defmodule Berrypod.Theme.CSSGenerator do
|
||||
|
||||
defp generate_mood("warm") do
|
||||
"""
|
||||
color-scheme: light;
|
||||
--t-surface-base: #fdf8f3;
|
||||
--t-surface-raised: #fffcf8;
|
||||
--t-surface-sunken: #f5ebe0;
|
||||
@ -91,6 +93,7 @@ defmodule Berrypod.Theme.CSSGenerator do
|
||||
|
||||
defp generate_mood("cool") do
|
||||
"""
|
||||
color-scheme: light;
|
||||
--t-surface-base: #f4f7fb;
|
||||
--t-surface-raised: #f8fafc;
|
||||
--t-surface-sunken: #e8eff7;
|
||||
@ -106,6 +109,7 @@ defmodule Berrypod.Theme.CSSGenerator do
|
||||
|
||||
defp generate_mood("dark") do
|
||||
"""
|
||||
color-scheme: dark;
|
||||
--t-surface-base: #0a0a0a;
|
||||
--t-surface-raised: #171717;
|
||||
--t-surface-sunken: #000000;
|
||||
@ -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}
|
||||
else
|
||||
s = if l > 0.5, do: delta / (2 - max - min), else: delta / (max + min)
|
||||
# oklab to oklch
|
||||
c = :math.sqrt(ok_a * ok_a + ok_b * ok_b)
|
||||
|
||||
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
|
||||
if c < 0.0001 do
|
||||
0.0
|
||||
else
|
||||
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
|
||||
|
||||
{h * 60, s}
|
||||
# Return as {lightness%, chroma, hue°} rounded for CSS
|
||||
{
|
||||
Float.round(ok_l * 100, 2),
|
||||
Float.round(c, 4),
|
||||
Float.round(h, 2)
|
||||
}
|
||||
end
|
||||
|
||||
{round(h), round(s * 100), round(l * 100)}
|
||||
end
|
||||
# Handle invalid hex values (neutral grey fallback)
|
||||
defp hex_to_oklch(_), do: {50.0, 0.0, 0.0}
|
||||
|
||||
# Handle invalid hex values
|
||||
defp hex_to_hsl(_), do: {0, 0, 50}
|
||||
# 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)
|
||||
|
||||
@ -76,15 +76,15 @@ defmodule Berrypod.SettingsTest do
|
||||
# Cache should now contain new CSS with the red accent color
|
||||
{:ok, updated_css} = CSSCache.get()
|
||||
assert updated_css =~ ".themed {"
|
||||
# Red = hue 0
|
||||
assert updated_css =~ "--t-accent-h: 0"
|
||||
# Red in oklch ≈ hue 29°
|
||||
assert updated_css =~ "--t-accent-h: 29.23"
|
||||
|
||||
# Change to blue
|
||||
{:ok, _settings} = Settings.update_theme_settings(%{accent_color: "#0000ff"})
|
||||
|
||||
{:ok, blue_css} = CSSCache.get()
|
||||
# Blue = hue 240
|
||||
assert blue_css =~ "--t-accent-h: 240"
|
||||
# Blue in oklch ≈ hue 264°
|
||||
assert blue_css =~ "--t-accent-h: 264.05"
|
||||
|
||||
# Restore default
|
||||
CSSCache.warm()
|
||||
|
||||
@ -12,44 +12,54 @@ defmodule Berrypod.Theme.CSSGeneratorTest do
|
||||
assert is_binary(css)
|
||||
# CSS targets .themed (used by both shop and preview)
|
||||
assert css =~ ".themed {"
|
||||
assert css =~ "--t-accent-h:"
|
||||
assert css =~ "--t-accent-s:"
|
||||
assert css =~ "--t-accent-l:"
|
||||
assert css =~ "--t-accent-c:"
|
||||
assert css =~ "--t-accent-h:"
|
||||
# Should include all theme token categories
|
||||
assert css =~ "--t-surface-base:"
|
||||
assert css =~ "--t-font-heading:"
|
||||
assert css =~ "--t-radius-sm:"
|
||||
assert css =~ "--t-density:"
|
||||
# Should include color-scheme
|
||||
assert css =~ "color-scheme:"
|
||||
end
|
||||
|
||||
test "converts hex colors to HSL" do
|
||||
test "converts hex colours to oklch" do
|
||||
settings = %ThemeSettings{accent_color: "#ff0000"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
# Red should be H=0, S=100%, L=50%
|
||||
assert css =~ "--t-accent-h: 0"
|
||||
assert css =~ "--t-accent-s: 100%"
|
||||
assert css =~ "--t-accent-l: 50%"
|
||||
# Red in oklch: L≈62.8%, C≈0.2577, H≈29.23°
|
||||
assert css =~ "--t-accent-l: 62.8"
|
||||
assert css =~ "--t-accent-c: 0.257"
|
||||
assert css =~ "--t-accent-h: 29.2"
|
||||
end
|
||||
|
||||
test "handles blue accent color" do
|
||||
test "handles blue accent colour" do
|
||||
settings = %ThemeSettings{accent_color: "#0000ff"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
# Blue should be H=240, S=100%, L=50%
|
||||
assert css =~ "--t-accent-h: 240"
|
||||
assert css =~ "--t-accent-s: 100%"
|
||||
assert css =~ "--t-accent-l: 50%"
|
||||
# Blue in oklch: L≈45.2%, C≈0.3132, H≈264.05°
|
||||
assert css =~ "--t-accent-l: 45.2"
|
||||
assert css =~ "--t-accent-c: 0.313"
|
||||
assert css =~ "--t-accent-h: 264."
|
||||
end
|
||||
|
||||
test "handles green accent color" do
|
||||
test "handles green accent colour" do
|
||||
settings = %ThemeSettings{accent_color: "#00ff00"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
# Green should be H=120, S=100%, L=50%
|
||||
assert css =~ "--t-accent-h: 120"
|
||||
assert css =~ "--t-accent-s: 100%"
|
||||
assert css =~ "--t-accent-l: 50%"
|
||||
# Green in oklch: L≈86.6%, C≈0.2948, H≈142.5°
|
||||
assert css =~ "--t-accent-l: 86.6"
|
||||
assert css =~ "--t-accent-c: 0.294"
|
||||
assert css =~ "--t-accent-h: 142."
|
||||
end
|
||||
|
||||
test "sets color-scheme based on mood" do
|
||||
light = CSSGenerator.generate(%ThemeSettings{mood: "neutral"})
|
||||
dark = CSSGenerator.generate(%ThemeSettings{mood: "dark"})
|
||||
|
||||
assert light =~ "color-scheme: light"
|
||||
assert dark =~ "color-scheme: dark"
|
||||
end
|
||||
|
||||
test "includes secondary colors" do
|
||||
|
||||
@ -145,9 +145,9 @@ defmodule BerrypodWeb.ThemeCSSConsistencyTest do
|
||||
assert css =~ "--space-lg:"
|
||||
|
||||
# Slider-controlled values
|
||||
assert css =~ "--t-accent-h:"
|
||||
assert css =~ "--t-accent-s:"
|
||||
assert css =~ "--t-accent-l:"
|
||||
assert css =~ "--t-accent-c:"
|
||||
assert css =~ "--t-accent-h:"
|
||||
assert css =~ "--t-font-size-scale:"
|
||||
assert css =~ "--t-heading-weight-override:"
|
||||
end
|
||||
|
||||
Loading…
Reference in New Issue
Block a user