refactor: consolidate CSS to use .themed class with native nesting

- Add .themed class as shared selector for both shop and preview
- Move visual/behavioral styles from .preview-frame to .themed
- Keep .preview-frame only for CSS variable switching (editor live preview)
- Update CSSGenerator to target .themed instead of .shop-root
- Refactor CSS files to use native CSS nesting syntax
- Update tests to reflect new class structure

This improves maintainability by:
- Eliminating duplicate selectors (.shop-root + .preview-frame)
- Using modern CSS nesting (94%+ browser support)
- Clear separation: .preview-frame = vars, .themed = styles

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-17 21:43:26 +00:00
parent 7491c34723
commit 75206474a1
8 changed files with 838 additions and 673 deletions

View File

@@ -4,6 +4,10 @@ defmodule SimpleshopTheme.Theme.CSSGenerator do
This module converts ThemeSettings into CSS variables that bridge the gap
between fixed primitives (Layer 1) and semantic aliases (Layer 3).
For the shop (public pages), this generates ALL theme tokens inline, so the
shop doesn't need the attribute-based selectors in theme-layer2-attributes.css.
The theme editor still uses those selectors for live preview switching.
"""
alias SimpleshopTheme.Settings.ThemeSettings
@@ -12,11 +16,26 @@ defmodule SimpleshopTheme.Theme.CSSGenerator do
Generates CSS for theme settings.
Returns a string of CSS custom properties that can be injected into a <style> tag.
This includes ALL theme tokens (mood, typography, shape, density) so the shop
pages don't need the attribute-based CSS selectors.
"""
def generate(%ThemeSettings{} = settings) do
"""
/* Theme Tokens - Layer 2 (dynamically generated) */
.preview-frame, .shop-root {
.themed {
/* Mood colors */
#{generate_mood(settings.mood)}
/* Typography */
#{generate_typography(settings.typography)}
/* Shape (border radii) */
#{generate_shape(settings.shape)}
/* Density */
#{generate_density(settings.density)}
/* Slider-controlled values */
#{generate_accent(settings.accent_color)}
#{generate_secondary_colors(settings)}
#{generate_font_size(settings.font_size)}
@@ -30,6 +49,229 @@ defmodule SimpleshopTheme.Theme.CSSGenerator do
|> String.trim()
end
# Mood colors - surface, text, and border colors
defp generate_mood("neutral") do
"""
--t-surface-base: #ffffff;
--t-surface-raised: #ffffff;
--t-surface-sunken: #f5f5f5;
--t-surface-overlay: rgba(255, 255, 255, 0.95);
--t-text-primary: #171717;
--t-text-secondary: #525252;
--t-text-tertiary: #a3a3a3;
--t-text-inverse: #ffffff;
--t-border-default: #e5e5e5;
--t-border-subtle: #f0f0f0;
"""
end
defp generate_mood("warm") do
"""
--t-surface-base: #fdf8f3;
--t-surface-raised: #fffcf8;
--t-surface-sunken: #f5ebe0;
--t-surface-overlay: rgba(253, 248, 243, 0.95);
--t-text-primary: #1c1917;
--t-text-secondary: #57534e;
--t-text-tertiary: #a8a29e;
--t-text-inverse: #ffffff;
--t-border-default: #e7e0d8;
--t-border-subtle: #f0ebe4;
"""
end
defp generate_mood("cool") do
"""
--t-surface-base: #f4f7fb;
--t-surface-raised: #f8fafc;
--t-surface-sunken: #e8eff7;
--t-surface-overlay: rgba(244, 247, 251, 0.95);
--t-text-primary: #0f172a;
--t-text-secondary: #475569;
--t-text-tertiary: #94a3b8;
--t-text-inverse: #ffffff;
--t-border-default: #d4dce8;
--t-border-subtle: #e8eff5;
"""
end
defp generate_mood("dark") do
"""
--t-surface-base: #0a0a0a;
--t-surface-raised: #171717;
--t-surface-sunken: #000000;
--t-surface-overlay: rgba(23, 23, 23, 0.95);
--t-text-primary: #fafafa;
--t-text-secondary: #a3a3a3;
--t-text-tertiary: #737373;
--t-text-inverse: #171717;
--t-border-default: #262626;
--t-border-subtle: #1c1c1c;
"""
end
# Fallback for any other mood value
defp generate_mood(_), do: generate_mood("neutral")
# Typography - font families, weights, and tracking
defp generate_typography("clean") do
"""
--t-font-heading: 'Manrope', system-ui, sans-serif;
--t-font-body: 'Inter', system-ui, sans-serif;
--t-heading-weight: 600;
--t-heading-tracking: -0.02em;
"""
end
defp generate_typography("editorial") do
"""
--t-font-heading: 'Playfair Display', Georgia, serif;
--t-font-body: 'Raleway', system-ui, sans-serif;
--t-heading-weight: 500;
--t-heading-tracking: -0.01em;
"""
end
defp generate_typography("modern") do
"""
--t-font-heading: 'Space Grotesk', system-ui, sans-serif;
--t-font-body: 'Inter', system-ui, sans-serif;
--t-heading-weight: 500;
--t-heading-tracking: -0.03em;
"""
end
defp generate_typography("classic") do
"""
--t-font-heading: 'Cormorant Garamond', Georgia, serif;
--t-font-body: 'Source Serif 4', Georgia, serif;
--t-heading-weight: 500;
--t-heading-tracking: 0;
"""
end
defp generate_typography("friendly") do
"""
--t-font-heading: 'Fraunces', Georgia, serif;
--t-font-body: 'Work Sans', system-ui, sans-serif;
--t-heading-weight: 600;
--t-heading-tracking: -0.01em;
"""
end
defp generate_typography("minimal") do
"""
--t-font-heading: 'DM Sans', system-ui, sans-serif;
--t-font-body: 'Source Serif 4', Georgia, serif;
--t-heading-weight: 500;
--t-heading-tracking: 0;
"""
end
defp generate_typography("impulse") do
"""
--t-font-heading: 'Raleway', system-ui, sans-serif;
--t-font-body: 'Inter', system-ui, sans-serif;
--t-heading-weight: 300;
--t-heading-tracking: 0.02em;
"""
end
# Fallback for any other typography value
defp generate_typography(_), do: generate_typography("clean")
# Shape - border radii
defp generate_shape("soft") do
"""
--t-radius-sm: 0.25rem;
--t-radius-md: 0.5rem;
--t-radius-lg: 0.75rem;
--t-radius-button: 0.5rem;
--t-radius-card: 0.75rem;
--t-radius-input: 0.5rem;
--t-radius-image: 0.5rem;
"""
end
defp generate_shape("sharp") do
"""
--t-radius-sm: 0;
--t-radius-md: 0;
--t-radius-lg: 0;
--t-radius-button: 0;
--t-radius-card: 0;
--t-radius-input: 0;
--t-radius-image: 0;
"""
end
defp generate_shape("round") do
"""
--t-radius-sm: 0.5rem;
--t-radius-md: 0.75rem;
--t-radius-lg: 1rem;
--t-radius-button: 0.75rem;
--t-radius-card: 1rem;
--t-radius-input: 0.75rem;
--t-radius-image: 0.75rem;
"""
end
defp generate_shape("pill") do
"""
--t-radius-sm: 9999px;
--t-radius-md: 9999px;
--t-radius-lg: 1rem;
--t-radius-button: 9999px;
--t-radius-card: 1rem;
--t-radius-input: 9999px;
--t-radius-image: 0.75rem;
"""
end
# Fallback for any other shape value
defp generate_shape(_), do: generate_shape("soft")
# Density - spacing multiplier
defp generate_density("balanced") do
"""
--t-density: 1;
--space-xs: 0.5rem;
--space-sm: 0.75rem;
--space-md: 1rem;
--space-lg: 1.5rem;
--space-xl: 2rem;
--space-2xl: 3rem;
"""
end
defp generate_density("spacious") do
"""
--t-density: 1.25;
--space-xs: 0.625rem;
--space-sm: 0.9375rem;
--space-md: 1.25rem;
--space-lg: 1.875rem;
--space-xl: 2.5rem;
--space-2xl: 3.75rem;
"""
end
defp generate_density("compact") do
"""
--t-density: 0.85;
--space-xs: 0.425rem;
--space-sm: 0.6375rem;
--space-md: 0.85rem;
--space-lg: 1.275rem;
--space-xl: 1.7rem;
--space-2xl: 2.55rem;
"""
end
# Fallback for any other density value
defp generate_density(_), do: generate_density("balanced")
# Accent color with HSL breakdown
defp generate_accent(hex_color) do
{h, s, l} = hex_to_hsl(hex_color)

View File

@@ -19,7 +19,7 @@
</head>
<body class="h-full">
<div
class="shop-root h-full"
class="themed shop-root h-full"
data-mood={@theme_settings.mood}
data-typography={@theme_settings.typography}
data-shape={@theme_settings.shape}
@@ -29,6 +29,7 @@
data-sticky={to_string(@theme_settings.sticky_header)}
data-layout={@theme_settings.layout_width}
data-shadow={@theme_settings.card_shadow}
data-button-style={@theme_settings.button_style}
>
<%= @inner_content %>
</div>

View File

@@ -888,7 +888,7 @@
</div>
<!-- Preview Frame -->
<div class="preview-frame bg-white overflow-auto flex-1 rounded-b-lg border border-t-0 border-base-content/20"
<div class="themed preview-frame bg-white overflow-auto flex-1 rounded-b-lg border border-t-0 border-base-content/20"
data-mood={@theme_settings.mood}
data-typography={@theme_settings.typography}
data-shape={@theme_settings.shape}