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:
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user