fix: add data attributes and Google Fonts to enable theme visual changes
- Add Google Fonts link to root layout for typography presets - Add data-mood, data-typography, data-shape, data-density attributes to preview-frame - Create theme-layer2-attributes.css with attribute-based CSS from demo - Move theme CSS files from priv/static/css to assets/css for proper compilation - Update CSS import order (primitives → layer2 → semantic) - Add 'css' to static_paths to serve theme CSS files This fixes the issue where theme controls updated the database but didn't visually affect the preview. The demo's attribute-based CSS system is now properly integrated with the Phoenix LiveView implementation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
6a3069f854
commit
476ec9667a
@ -103,9 +103,12 @@
|
|||||||
[data-phx-session], [data-phx-teleported-src] { display: contents }
|
[data-phx-session], [data-phx-teleported-src] { display: contents }
|
||||||
|
|
||||||
/* Theme CSS - Layer 1: Primitives (fixed CSS variables) */
|
/* Theme CSS - Layer 1: Primitives (fixed CSS variables) */
|
||||||
@import url("/css/theme-primitives.css");
|
@import "./theme-primitives.css";
|
||||||
|
|
||||||
|
/* Theme CSS - Layer 2: Attribute-based theme tokens */
|
||||||
|
@import "./theme-layer2-attributes.css";
|
||||||
|
|
||||||
/* Theme CSS - Layer 3: Semantic aliases */
|
/* Theme CSS - Layer 3: Semantic aliases */
|
||||||
@import url("/css/theme-semantic.css");
|
@import "./theme-semantic.css";
|
||||||
|
|
||||||
/* This file is for your main application CSS */
|
/* This file is for your main application CSS */
|
||||||
|
|||||||
157
assets/css/theme-layer2-attributes.css
Normal file
157
assets/css/theme-layer2-attributes.css
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
/* ========================================
|
||||||
|
LAYER 2: THEME TOKENS (Attribute-based)
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
/* Mood - Default (Neutral) */
|
||||||
|
.preview-frame {
|
||||||
|
--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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-frame[data-mood="warm"] {
|
||||||
|
--t-surface-base: #fdf8f3;
|
||||||
|
--t-surface-raised: #fffcf8;
|
||||||
|
--t-surface-sunken: #f5ebe0;
|
||||||
|
--t-text-primary: #1c1917;
|
||||||
|
--t-text-secondary: #57534e;
|
||||||
|
--t-text-tertiary: #a8a29e;
|
||||||
|
--t-border-default: #e7e0d8;
|
||||||
|
--t-border-subtle: #f0ebe4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-frame[data-mood="cool"] {
|
||||||
|
--t-surface-base: #f4f7fb;
|
||||||
|
--t-surface-raised: #f8fafc;
|
||||||
|
--t-surface-sunken: #e8eff7;
|
||||||
|
--t-text-primary: #0f172a;
|
||||||
|
--t-text-secondary: #475569;
|
||||||
|
--t-text-tertiary: #94a3b8;
|
||||||
|
--t-border-default: #d4dce8;
|
||||||
|
--t-border-subtle: #e8eff5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-frame[data-mood="dark"] {
|
||||||
|
--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;
|
||||||
|
--p-shadow-strength: 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typography - Default (Clean/Inter) */
|
||||||
|
.preview-frame {
|
||||||
|
--t-font-heading: var(--p-font-inter);
|
||||||
|
--t-font-body: var(--p-font-inter);
|
||||||
|
--t-heading-weight: 600;
|
||||||
|
--t-heading-tracking: -0.025em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-frame[data-typography="editorial"] {
|
||||||
|
--t-font-heading: var(--p-font-fraunces);
|
||||||
|
--t-font-body: var(--p-font-source);
|
||||||
|
--t-heading-weight: 600;
|
||||||
|
--t-heading-tracking: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-frame[data-typography="modern"] {
|
||||||
|
--t-font-heading: var(--p-font-space);
|
||||||
|
--t-font-body: var(--p-font-space);
|
||||||
|
--t-heading-weight: 500;
|
||||||
|
--t-heading-tracking: -0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-frame[data-typography="classic"] {
|
||||||
|
--t-font-heading: var(--p-font-baskerville);
|
||||||
|
--t-font-body: var(--p-font-source);
|
||||||
|
--t-heading-weight: 400;
|
||||||
|
--t-heading-tracking: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-frame[data-typography="friendly"] {
|
||||||
|
--t-font-heading: var(--p-font-nunito);
|
||||||
|
--t-font-body: var(--p-font-nunito);
|
||||||
|
--t-heading-weight: 700;
|
||||||
|
--t-heading-tracking: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-frame[data-typography="minimal"] {
|
||||||
|
--t-font-heading: var(--p-font-outfit);
|
||||||
|
--t-font-body: var(--p-font-outfit);
|
||||||
|
--t-heading-weight: 300;
|
||||||
|
--t-heading-tracking: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-frame[data-typography="impulse"] {
|
||||||
|
--t-font-heading: var(--p-font-avenir);
|
||||||
|
--t-font-body: var(--p-font-avenir);
|
||||||
|
--t-heading-weight: 300;
|
||||||
|
--t-heading-tracking: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Shape - Default (Soft) */
|
||||||
|
.preview-frame {
|
||||||
|
--t-radius-sm: var(--p-radius-sm);
|
||||||
|
--t-radius-md: var(--p-radius-md);
|
||||||
|
--t-radius-lg: var(--p-radius-lg);
|
||||||
|
--t-radius-button: var(--p-radius-md);
|
||||||
|
--t-radius-card: var(--p-radius-lg);
|
||||||
|
--t-radius-input: var(--p-radius-md);
|
||||||
|
--t-radius-image: var(--p-radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-frame[data-shape="sharp"] {
|
||||||
|
--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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-frame[data-shape="round"] {
|
||||||
|
--t-radius-sm: var(--p-radius-md);
|
||||||
|
--t-radius-md: var(--p-radius-lg);
|
||||||
|
--t-radius-lg: var(--p-radius-xl);
|
||||||
|
--t-radius-button: var(--p-radius-lg);
|
||||||
|
--t-radius-card: var(--p-radius-xl);
|
||||||
|
--t-radius-input: var(--p-radius-lg);
|
||||||
|
--t-radius-image: var(--p-radius-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-frame[data-shape="pill"] {
|
||||||
|
--t-radius-sm: var(--p-radius-full);
|
||||||
|
--t-radius-md: var(--p-radius-full);
|
||||||
|
--t-radius-lg: var(--p-radius-xl);
|
||||||
|
--t-radius-button: var(--p-radius-full);
|
||||||
|
--t-radius-card: var(--p-radius-xl);
|
||||||
|
--t-radius-input: var(--p-radius-full);
|
||||||
|
--t-radius-image: var(--p-radius-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Density - Default (Balanced) */
|
||||||
|
.preview-frame {
|
||||||
|
--t-density: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-frame[data-density="spacious"] {
|
||||||
|
--t-density: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-frame[data-density="compact"] {
|
||||||
|
--t-density: 0.85;
|
||||||
|
}
|
||||||
@ -17,7 +17,7 @@ defmodule SimpleshopThemeWeb do
|
|||||||
those modules here.
|
those modules here.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
|
def static_paths, do: ~w(assets css fonts images favicon.ico robots.txt)
|
||||||
|
|
||||||
def router do
|
def router do
|
||||||
quote do
|
quote do
|
||||||
|
|||||||
@ -8,6 +8,9 @@
|
|||||||
{assigns[:page_title]}
|
{assigns[:page_title]}
|
||||||
</.live_title>
|
</.live_title>
|
||||||
<link phx-track-static rel="stylesheet" href={~p"/assets/css/app.css"} />
|
<link phx-track-static rel="stylesheet" href={~p"/assets/css/app.css"} />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,500;9..144,600;9..144,700&family=Inter:wght@400;500;600;700&family=Libre+Baskerville:wght@400;700&family=Nunito:wght@400;600;700&family=Nunito+Sans:opsz,wght@6..12,300;6..12,400;6..12,500;6..12,600&family=Outfit:wght@300;400;500;600&family=Source+Sans+3:wght@400;500;600&family=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet">
|
||||||
<script defer phx-track-static type="text/javascript" src={~p"/assets/js/app.js"}>
|
<script defer phx-track-static type="text/javascript" src={~p"/assets/js/app.js"}>
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@ -53,6 +53,76 @@ defmodule SimpleshopThemeWeb.ThemeLive.Index do
|
|||||||
{:noreply, assign(socket, :preview_page, page_atom)}
|
{:noreply, assign(socket, :preview_page, page_atom)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("update_setting", %{"field" => field, "setting_value" => value}, socket) do
|
||||||
|
field_atom = String.to_existing_atom(field)
|
||||||
|
attrs = %{field_atom => value}
|
||||||
|
|
||||||
|
case Settings.update_theme_settings(attrs) do
|
||||||
|
{:ok, theme_settings} ->
|
||||||
|
generated_css = CSSGenerator.generate(theme_settings)
|
||||||
|
|
||||||
|
socket =
|
||||||
|
socket
|
||||||
|
|> assign(:theme_settings, theme_settings)
|
||||||
|
|> assign(:generated_css, generated_css)
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
|
||||||
|
{:error, _} ->
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("update_setting", %{"field" => field} = params, socket) do
|
||||||
|
# For phx-change events from select/input elements, the value comes from the name attribute
|
||||||
|
value = params[field] || params["#{field}_text"] || params["value"]
|
||||||
|
|
||||||
|
if value do
|
||||||
|
field_atom = String.to_existing_atom(field)
|
||||||
|
attrs = %{field_atom => value}
|
||||||
|
|
||||||
|
case Settings.update_theme_settings(attrs) do
|
||||||
|
{:ok, theme_settings} ->
|
||||||
|
generated_css = CSSGenerator.generate(theme_settings)
|
||||||
|
|
||||||
|
socket =
|
||||||
|
socket
|
||||||
|
|> assign(:theme_settings, theme_settings)
|
||||||
|
|> assign(:generated_css, generated_css)
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
|
||||||
|
{:error, _} ->
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("update_color", %{"field" => field, "value" => value}, socket) do
|
||||||
|
field_atom = String.to_existing_atom(field)
|
||||||
|
attrs = %{field_atom => value}
|
||||||
|
|
||||||
|
case Settings.update_theme_settings(attrs) do
|
||||||
|
{:ok, theme_settings} ->
|
||||||
|
generated_css = CSSGenerator.generate(theme_settings)
|
||||||
|
|
||||||
|
socket =
|
||||||
|
socket
|
||||||
|
|> assign(:theme_settings, theme_settings)
|
||||||
|
|> assign(:generated_css, generated_css)
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
|
||||||
|
{:error, _} ->
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("save_theme", _params, socket) do
|
def handle_event("save_theme", _params, socket) do
|
||||||
socket = put_flash(socket, :info, "Theme saved successfully")
|
socket = put_flash(socket, :info, "Theme saved successfully")
|
||||||
|
|||||||
@ -35,38 +35,137 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Current Settings Display -->
|
<!-- Customization Controls -->
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<!-- Mood -->
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-lg font-semibold mb-4">Current Settings</h2>
|
<h3 class="font-semibold mb-3">Mood</h3>
|
||||||
<div class="space-y-2 text-sm">
|
<div class="grid grid-cols-2 gap-2">
|
||||||
<div class="flex justify-between">
|
<%= for mood <- ["neutral", "warm", "cool", "dark"] do %>
|
||||||
<span class="font-medium">Mood:</span>
|
<button
|
||||||
<span class="capitalize"><%= @theme_settings.mood %></span>
|
type="button"
|
||||||
</div>
|
phx-click="update_setting"
|
||||||
<div class="flex justify-between">
|
phx-value-field="mood"
|
||||||
<span class="font-medium">Typography:</span>
|
phx-value-setting_value={mood}
|
||||||
<span class="capitalize"><%= @theme_settings.typography %></span>
|
class={"btn btn-sm #{if @theme_settings.mood == mood, do: "btn-primary", else: "btn-outline"} capitalize"}
|
||||||
</div>
|
>
|
||||||
<div class="flex justify-between">
|
<%= mood %>
|
||||||
<span class="font-medium">Shape:</span>
|
</button>
|
||||||
<span class="capitalize"><%= @theme_settings.shape %></span>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
</div>
|
||||||
<span class="font-medium">Density:</span>
|
|
||||||
<span class="capitalize"><%= @theme_settings.density %></span>
|
<!-- Typography -->
|
||||||
</div>
|
<div>
|
||||||
<div class="flex justify-between">
|
<h3 class="font-semibold mb-3">Typography</h3>
|
||||||
<span class="font-medium">Accent Color:</span>
|
<form phx-change="update_setting" phx-value-field="typography">
|
||||||
<span>
|
<select
|
||||||
<span
|
name="typography"
|
||||||
class="inline-block w-4 h-4 rounded-sm border border-base-300"
|
class="select select-bordered select-sm w-full capitalize"
|
||||||
style={"background-color: #{@theme_settings.accent_color}"}
|
>
|
||||||
>
|
<%= for typo <- ["clean", "editorial", "modern", "classic", "friendly", "minimal", "impulse"] do %>
|
||||||
</span>
|
<option value={typo} selected={@theme_settings.typography == typo}>
|
||||||
<%= @theme_settings.accent_color %>
|
<%= typo %>
|
||||||
</span>
|
</option>
|
||||||
</div>
|
<% end %>
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Shape -->
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold mb-3">Shape</h3>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<%= for shape <- ["sharp", "soft", "round", "pill"] do %>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
phx-click="update_setting"
|
||||||
|
phx-value-field="shape"
|
||||||
|
phx-value-setting_value={shape}
|
||||||
|
class={"btn btn-sm #{if @theme_settings.shape == shape, do: "btn-primary", else: "btn-outline"} capitalize"}
|
||||||
|
>
|
||||||
|
<%= shape %>
|
||||||
|
</button>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Density -->
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold mb-3">Density</h3>
|
||||||
|
<div class="grid grid-cols-3 gap-2">
|
||||||
|
<%= for density <- ["spacious", "balanced", "compact"] do %>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
phx-click="update_setting"
|
||||||
|
phx-value-field="density"
|
||||||
|
phx-value-setting_value={density}
|
||||||
|
class={"btn btn-sm #{if @theme_settings.density == density, do: "btn-primary", else: "btn-outline"} capitalize text-xs"}
|
||||||
|
>
|
||||||
|
<%= density %>
|
||||||
|
</button>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Grid Columns -->
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold mb-3">Grid Columns</h3>
|
||||||
|
<div class="grid grid-cols-3 gap-2">
|
||||||
|
<%= for cols <- ["2", "3", "4"] do %>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
phx-click="update_setting"
|
||||||
|
phx-value-field="grid_columns"
|
||||||
|
phx-value-setting_value={cols}
|
||||||
|
class={"btn btn-sm #{if @theme_settings.grid_columns == cols, do: "btn-primary", else: "btn-outline"}"}
|
||||||
|
>
|
||||||
|
<%= cols %>
|
||||||
|
</button>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Colors -->
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold mb-3">Accent Color</h3>
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
phx-change="update_color"
|
||||||
|
phx-value-field="accent_color"
|
||||||
|
value={@theme_settings.accent_color}
|
||||||
|
class="w-12 h-10 rounded cursor-pointer"
|
||||||
|
phx-debounce="300"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
phx-change="update_color"
|
||||||
|
phx-value-field="accent_color"
|
||||||
|
value={@theme_settings.accent_color}
|
||||||
|
class="input input-bordered input-sm flex-1 font-mono text-xs"
|
||||||
|
placeholder="#000000"
|
||||||
|
phx-debounce="blur"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header Layout -->
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold mb-3">Header Layout</h3>
|
||||||
|
<div class="grid grid-cols-3 gap-2">
|
||||||
|
<%= for layout <- ["standard", "centered", "minimal"] do %>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
phx-click="update_setting"
|
||||||
|
phx-value-field="header_layout"
|
||||||
|
phx-value-setting_value={layout}
|
||||||
|
class={"btn btn-sm #{if @theme_settings.header_layout == layout, do: "btn-primary", else: "btn-outline"} capitalize text-xs"}
|
||||||
|
>
|
||||||
|
<%= layout %>
|
||||||
|
</button>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -103,7 +202,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Preview Frame -->
|
<!-- Preview Frame -->
|
||||||
<div class="preview-frame bg-white overflow-auto" style="min-height: 600px; max-height: calc(100vh - 200px);">
|
<div class="preview-frame bg-white overflow-auto"
|
||||||
|
data-mood={@theme_settings.mood}
|
||||||
|
data-typography={@theme_settings.typography}
|
||||||
|
data-shape={@theme_settings.shape}
|
||||||
|
data-density={@theme_settings.density}
|
||||||
|
data-grid={@theme_settings.grid_columns}
|
||||||
|
data-header={@theme_settings.header_layout}
|
||||||
|
style="min-height: 600px; max-height: calc(100vh - 200px);">
|
||||||
<style>
|
<style>
|
||||||
<%= Phoenix.HTML.raw(@generated_css) %>
|
<%= Phoenix.HTML.raw(@generated_css) %>
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -114,5 +114,102 @@ defmodule SimpleshopThemeWeb.ThemeLiveTest do
|
|||||||
assert html =~ "Contact"
|
assert html =~ "Contact"
|
||||||
assert html =~ "404"
|
assert html =~ "404"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "mood customization buttons work", %{conn: conn} do
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/admin/theme")
|
||||||
|
|
||||||
|
# Click the "dark" mood button
|
||||||
|
html =
|
||||||
|
view
|
||||||
|
|> element("button", "dark")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Verify the setting was updated
|
||||||
|
theme_settings = Settings.get_theme_settings()
|
||||||
|
assert theme_settings.mood == "dark"
|
||||||
|
assert html =~ "dark"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "shape customization buttons work", %{conn: conn} do
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/admin/theme")
|
||||||
|
|
||||||
|
# Click the "round" shape button
|
||||||
|
view
|
||||||
|
|> element("button", "round")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Verify the setting was updated
|
||||||
|
theme_settings = Settings.get_theme_settings()
|
||||||
|
assert theme_settings.shape == "round"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "density customization buttons work", %{conn: conn} do
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/admin/theme")
|
||||||
|
|
||||||
|
# Click the "compact" density button
|
||||||
|
view
|
||||||
|
|> element("button", "compact")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Verify the setting was updated
|
||||||
|
theme_settings = Settings.get_theme_settings()
|
||||||
|
assert theme_settings.density == "compact"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "grid columns customization buttons work", %{conn: conn} do
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/admin/theme")
|
||||||
|
|
||||||
|
# Click the "2" grid columns button
|
||||||
|
view
|
||||||
|
|> element("button", "2")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Verify the setting was updated
|
||||||
|
theme_settings = Settings.get_theme_settings()
|
||||||
|
assert theme_settings.grid_columns == "2"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "typography customization select works", %{conn: conn} do
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/admin/theme")
|
||||||
|
|
||||||
|
# Change typography via the form
|
||||||
|
view
|
||||||
|
|> element("form[phx-value-field='typography']")
|
||||||
|
|> render_change(%{"typography" => "modern"})
|
||||||
|
|
||||||
|
# Verify the setting was updated
|
||||||
|
theme_settings = Settings.get_theme_settings()
|
||||||
|
assert theme_settings.typography == "modern"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "header layout customization buttons work", %{conn: conn} do
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/admin/theme")
|
||||||
|
|
||||||
|
# Click the "centered" header layout button
|
||||||
|
view
|
||||||
|
|> element("button", "centered")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Verify the setting was updated
|
||||||
|
theme_settings = Settings.get_theme_settings()
|
||||||
|
assert theme_settings.header_layout == "centered"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "CSS regenerates when settings change", %{conn: conn} do
|
||||||
|
{:ok, view, html} = live(conn, ~p"/admin/theme")
|
||||||
|
|
||||||
|
# Capture initial CSS
|
||||||
|
initial_css = html
|
||||||
|
|
||||||
|
# Change a setting
|
||||||
|
new_html =
|
||||||
|
view
|
||||||
|
|> element("button", "dark")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Verify CSS has changed (dark mode should have different surface colors)
|
||||||
|
refute initial_css == new_html
|
||||||
|
assert new_html =~ "--t-surface-base:"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user