feat: add dark mode support, accordion UI, and current combination display

- Update Theme Studio sidebar to use DaisyUI theme-aware classes for dark mode
- Convert Customise accordion to native details/summary elements for proper interaction
- Add "Current combination" card showing active theme settings
- Add SVG recolorer for logo color customization
- Add image controller for serving uploaded images
- Implement header background image controls (zoom, position)
- Add toggle_customise event handler to preserve accordion state across re-renders

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-31 18:55:44 +00:00
parent 0dada968aa
commit 1ca703e548
20 changed files with 1477 additions and 318 deletions

View File

@@ -0,0 +1,93 @@
defmodule SimpleshopThemeWeb.ImageController do
use SimpleshopThemeWeb, :controller
alias SimpleshopTheme.Media
alias SimpleshopTheme.Media.SVGRecolorer
@doc """
Serves an image from the database by ID.
Images are served with aggressive caching headers since they are
immutable once uploaded.
"""
def show(conn, %{"id" => id}) do
case Media.get_image(id) do
nil ->
send_resp(conn, 404, "Image not found")
image ->
conn
|> put_resp_content_type(image.content_type)
|> put_resp_header("cache-control", "public, max-age=31536000, immutable")
|> put_resp_header("etag", ~s("#{image.id}"))
|> send_resp(200, image.data)
end
end
@doc """
Serves a thumbnail of an image if available, otherwise falls back to full image.
"""
def thumbnail(conn, %{"id" => id}) do
case Media.get_image(id) do
nil ->
send_resp(conn, 404, "Image not found")
%{thumbnail_data: thumbnail_data} = image when not is_nil(thumbnail_data) ->
conn
|> put_resp_content_type("image/jpeg")
|> put_resp_header("cache-control", "public, max-age=31536000, immutable")
|> put_resp_header("etag", ~s("#{image.id}-thumb"))
|> send_resp(200, thumbnail_data)
image ->
conn
|> put_resp_content_type(image.content_type)
|> put_resp_header("cache-control", "public, max-age=31536000, immutable")
|> put_resp_header("etag", ~s("#{image.id}"))
|> send_resp(200, image.data)
end
end
@doc """
Serves an SVG image recolored with the specified color.
The color should be a hex color code (with or without the leading #).
Only works with SVG images.
"""
def recolored_svg(conn, %{"id" => id, "color" => color}) do
clean_color = normalize_color(color)
with true <- SVGRecolorer.valid_hex_color?(clean_color),
%{is_svg: true, svg_content: svg} when not is_nil(svg) <- Media.get_image(id) do
recolored = SVGRecolorer.recolor(svg, clean_color)
conn
|> put_resp_content_type("image/svg+xml")
|> put_resp_header("cache-control", "public, max-age=3600")
|> put_resp_header("etag", ~s("#{id}-#{clean_color}"))
|> send_resp(200, recolored)
else
false ->
send_resp(conn, 400, "Invalid color format")
nil ->
send_resp(conn, 404, "Image not found")
%{is_svg: false} ->
send_resp(conn, 400, "Image is not an SVG")
%{svg_content: nil} ->
send_resp(conn, 400, "SVG content not available")
end
end
defp normalize_color(color) do
color = String.trim(color)
if String.starts_with?(color, "#") do
color
else
"#" <> color
end
end
end

View File

@@ -2,12 +2,14 @@ defmodule SimpleshopThemeWeb.ThemeLive.Index do
use SimpleshopThemeWeb, :live_view
alias SimpleshopTheme.Settings
alias SimpleshopTheme.Media
alias SimpleshopTheme.Theme.{CSSGenerator, Presets, PreviewData}
@impl true
def mount(_params, _session, socket) do
theme_settings = Settings.get_theme_settings()
generated_css = CSSGenerator.generate(theme_settings)
active_preset = Presets.detect_preset(theme_settings)
preview_data = %{
products: PreviewData.products(),
cart_items: PreviewData.cart_items(),
@@ -15,17 +17,86 @@ defmodule SimpleshopThemeWeb.ThemeLive.Index do
categories: PreviewData.categories()
}
logo_image = Media.get_logo()
header_image = Media.get_header()
socket =
socket
|> assign(:theme_settings, theme_settings)
|> assign(:generated_css, generated_css)
|> assign(:preview_page, :home)
|> assign(:preset_names, Presets.list_names())
|> assign(:presets_with_descriptions, Presets.all_with_descriptions())
|> assign(:active_preset, active_preset)
|> assign(:preview_data, preview_data)
|> assign(:logo_image, logo_image)
|> assign(:header_image, header_image)
|> assign(:customise_open, false)
|> allow_upload(:logo_upload,
accept: ~w(.png .jpg .jpeg .webp .svg),
max_entries: 1,
max_file_size: 2_000_000,
auto_upload: true,
progress: &handle_progress/3
)
|> allow_upload(:header_upload,
accept: ~w(.png .jpg .jpeg .webp),
max_entries: 1,
max_file_size: 5_000_000,
auto_upload: true,
progress: &handle_progress/3
)
{:ok, socket}
end
defp handle_progress(:logo_upload, entry, socket) do
if entry.done? do
consume_uploaded_entries(socket, :logo_upload, fn %{path: path}, entry ->
case Media.upload_from_entry(path, entry, "logo") do
{:ok, image} ->
Settings.update_theme_settings(%{logo_image_id: image.id})
{:ok, image}
{:error, _} = error ->
error
end
end)
|> case do
[image | _] ->
{:noreply, assign(socket, :logo_image, image)}
_ ->
{:noreply, socket}
end
else
{:noreply, socket}
end
end
defp handle_progress(:header_upload, entry, socket) do
if entry.done? do
consume_uploaded_entries(socket, :header_upload, fn %{path: path}, entry ->
case Media.upload_from_entry(path, entry, "header") do
{:ok, image} ->
Settings.update_theme_settings(%{header_image_id: image.id})
{:ok, image}
{:error, _} = error ->
error
end
end)
|> case do
[image | _] ->
{:noreply, assign(socket, :header_image, image)}
_ ->
{:noreply, socket}
end
else
{:noreply, socket}
end
end
@impl true
def handle_event("apply_preset", %{"preset" => preset_name}, socket) do
preset_atom = String.to_existing_atom(preset_name)
@@ -38,6 +109,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.Index do
socket
|> assign(:theme_settings, theme_settings)
|> assign(:generated_css, generated_css)
|> assign(:active_preset, preset_atom)
|> put_flash(:info, "Applied #{preset_name} preset")
{:noreply, socket}
@@ -61,11 +133,13 @@ defmodule SimpleshopThemeWeb.ThemeLive.Index do
case Settings.update_theme_settings(attrs) do
{:ok, theme_settings} ->
generated_css = CSSGenerator.generate(theme_settings)
active_preset = Presets.detect_preset(theme_settings)
socket =
socket
|> assign(:theme_settings, theme_settings)
|> assign(:generated_css, generated_css)
|> assign(:active_preset, active_preset)
{:noreply, socket}
@@ -86,11 +160,13 @@ defmodule SimpleshopThemeWeb.ThemeLive.Index do
case Settings.update_theme_settings(attrs) do
{:ok, theme_settings} ->
generated_css = CSSGenerator.generate(theme_settings)
active_preset = Presets.detect_preset(theme_settings)
socket =
socket
|> assign(:theme_settings, theme_settings)
|> assign(:generated_css, generated_css)
|> assign(:active_preset, active_preset)
{:noreply, socket}
@@ -110,11 +186,13 @@ defmodule SimpleshopThemeWeb.ThemeLive.Index do
case Settings.update_theme_settings(attrs) do
{:ok, theme_settings} ->
generated_css = CSSGenerator.generate(theme_settings)
active_preset = Presets.detect_preset(theme_settings)
socket =
socket
|> assign(:theme_settings, theme_settings)
|> assign(:generated_css, generated_css)
|> assign(:active_preset, active_preset)
{:noreply, socket}
@@ -128,4 +206,57 @@ defmodule SimpleshopThemeWeb.ThemeLive.Index do
socket = put_flash(socket, :info, "Theme saved successfully")
{:noreply, socket}
end
@impl true
def handle_event("remove_logo", _params, socket) do
if logo = socket.assigns.logo_image do
Media.delete_image(logo)
end
Settings.update_theme_settings(%{logo_image_id: nil})
socket =
socket
|> assign(:logo_image, nil)
|> put_flash(:info, "Logo removed")
{:noreply, socket}
end
@impl true
def handle_event("remove_header", _params, socket) do
if header = socket.assigns.header_image do
Media.delete_image(header)
end
Settings.update_theme_settings(%{header_image_id: nil})
socket =
socket
|> assign(:header_image, nil)
|> put_flash(:info, "Header image removed")
{:noreply, socket}
end
@impl true
def handle_event("cancel_upload", %{"ref" => ref, "upload" => upload_name}, socket) do
upload_atom = String.to_existing_atom(upload_name)
{:noreply, cancel_upload(socket, upload_atom, ref)}
end
@impl true
def handle_event("toggle_customise", _params, socket) do
{:noreply, assign(socket, :customise_open, !socket.assigns.customise_open)}
end
@impl true
def handle_event("noop", _params, socket) do
{:noreply, socket}
end
def error_to_string(:too_large), do: "File is too large"
def error_to_string(:too_many_files), do: "Too many files"
def error_to_string(:not_accepted), do: "File type not accepted"
def error_to_string(err), do: inspect(err)
end

View File

@@ -1,234 +1,611 @@
<div class="min-h-screen bg-base-200">
<div class="navbar bg-base-100 shadow-sm">
<div class="flex-1">
<h1 class="text-xl font-bold px-4">Theme Editor</h1>
</div>
<div class="flex-none gap-2">
<button
type="button"
phx-click="save_theme"
class="btn btn-primary"
>
Save Theme
</button>
</div>
</div>
<div class="flex flex-col lg:flex-row">
<div class="flex flex-col lg:flex-row lg:h-screen">
<!-- Controls Sidebar -->
<div class="w-full lg:w-80 bg-base-100 p-6 shadow-lg overflow-y-auto lg:h-[calc(100vh-64px)]">
<div class="space-y-6">
<!-- Presets Section -->
<div>
<h2 class="text-lg font-semibold mb-4">Presets</h2>
<div class="grid grid-cols-2 gap-2">
<%= for preset_name <- @preset_names do %>
<button
type="button"
phx-click="apply_preset"
phx-value-preset={preset_name}
class="btn btn-sm btn-outline capitalize"
>
<%= preset_name %>
</button>
<% end %>
</div>
</div>
<div class="w-full lg:w-[380px] bg-base-100 border-r border-base-300 p-6 overflow-y-auto lg:h-screen flex-shrink-0">
<!-- Header -->
<div class="mb-6">
<h1 class="text-xl font-semibold tracking-tight mb-2 text-base-content">Theme Studio</h1>
<p class="text-sm text-base-content/60 leading-relaxed">One theme, infinite possibilities. Every combination is designed to work beautifully.</p>
</div>
<!-- Customization Controls -->
<div class="divider"></div>
<!-- Site Name -->
<div class="mb-6">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Shop name</label>
<form phx-change="update_setting" phx-value-field="site_name">
<input
type="text"
name="site_name"
value={@theme_settings.site_name}
placeholder="Your shop name"
class="w-full px-4 py-3 border border-base-300 rounded-lg text-base bg-base-100 text-base-content focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary/20 transition-all"
/>
</form>
</div>
<!-- Mood -->
<div>
<h3 class="font-semibold mb-3">Mood</h3>
<div class="grid grid-cols-2 gap-2">
<%= for mood <- ["neutral", "warm", "cool", "dark"] do %>
<button
type="button"
phx-click="update_setting"
phx-value-field="mood"
phx-value-setting_value={mood}
class={"btn btn-sm #{if @theme_settings.mood == mood, do: "btn-primary", else: "btn-outline"} capitalize"}
>
<%= mood %>
</button>
<% end %>
</div>
</div>
<!-- Branding Section (styled background box) -->
<div class="bg-base-200 rounded-xl p-4 mb-6">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-4">Logo & header</label>
<!-- Typography -->
<div>
<h3 class="font-semibold mb-3">Typography</h3>
<form phx-change="update_setting" phx-value-field="typography">
<select
name="typography"
class="select select-bordered select-sm w-full capitalize"
<!-- Logo Mode Radio Cards -->
<div class="flex flex-col gap-2 mb-4">
<%= for {value, title, desc} <- [
{"text-only", "Shop name only", "Your name in the heading font"},
{"logo-text", "Logo + shop name", "Your logo image with name beside it"},
{"logo-only", "Logo only", "Just your logo (with text built in)"}
] do %>
<label
class={[
"flex items-center gap-3 p-3 bg-base-100 border-2 rounded-lg cursor-pointer transition-all",
if(@theme_settings.logo_mode == value,
do: "border-base-content",
else: "border-transparent hover:border-base-300"
)
]}
>
<%= for typo <- ["clean", "editorial", "modern", "classic", "friendly", "minimal", "impulse"] do %>
<option value={typo} selected={@theme_settings.typography == typo}>
<%= typo %>
</option>
<input
type="radio"
name="logo_mode"
value={value}
checked={@theme_settings.logo_mode == value}
phx-click="update_setting"
phx-value-field="logo_mode"
phx-value-setting_value={value}
class="hidden"
/>
<span class={[
"w-[18px] h-[18px] rounded-full border-2 flex items-center justify-center flex-shrink-0 transition-all",
if(@theme_settings.logo_mode == value,
do: "border-base-content",
else: "border-base-300"
)
]}>
<span class={[
"w-2 h-2 rounded-full bg-base-content transition-all",
if(@theme_settings.logo_mode == value, do: "scale-100 opacity-100", else: "scale-0 opacity-0")
]}></span>
</span>
<div class="flex-1">
<div class="text-sm font-medium text-base-content"><%= title %></div>
<div class="text-xs text-base-content/60"><%= desc %></div>
</div>
</label>
<% end %>
</div>
<!-- Logo Upload (for logo-text and logo-only modes) -->
<%= if @theme_settings.logo_mode in ["logo-text", "logo-only"] do %>
<div class="mt-4 pt-4 border-t border-base-300">
<span class="block text-xs font-medium text-base-content/70 mb-2">Upload logo (SVG or PNG)</span>
<div class="flex items-center gap-3">
<form phx-change="noop" phx-submit="noop" class="flex-1">
<label class="flex-1 bg-base-100 border border-dashed border-base-300 rounded-lg p-3 text-sm text-base-content/60 text-center cursor-pointer hover:border-base-content/40 hover:text-base-content/80 transition-all">
<span>Choose file...</span>
<.live_file_input upload={@uploads.logo_upload} class="hidden" />
</label>
</form>
<%= if @logo_image do %>
<div class="relative w-16 h-10 bg-base-100 border border-base-300 rounded-lg flex items-center justify-center overflow-hidden">
<img
src={"/images/#{@logo_image.id}"}
alt="Current logo"
class="max-w-full max-h-full object-contain"
/>
<button
type="button"
phx-click="remove_logo"
class="absolute -top-1.5 -right-1.5 w-[18px] h-[18px] bg-base-content text-base-100 rounded-full text-xs flex items-center justify-center leading-none"
title="Remove logo"
>×</button>
</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>
<form id="accent-color-form" phx-change="update_color" phx-value-field="accent_color" phx-hook="ColorSync">
<div class="flex gap-2 items-center">
<input
type="color"
id="accent-color-picker"
name="value"
value={@theme_settings.accent_color}
class="w-12 h-10 rounded cursor-pointer"
/>
<input
type="text"
id="accent-color-text"
name="value"
value={@theme_settings.accent_color}
class="input input-bordered input-sm flex-1 font-mono text-xs"
placeholder="#000000"
/>
</div>
</form>
</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>
<%= for entry <- @uploads.logo_upload.entries do %>
<div class="flex items-center gap-2 mt-2">
<div class="flex-1 h-1.5 bg-base-300 rounded-full overflow-hidden">
<div class="h-full bg-primary transition-all" style={"width: #{entry.progress}%"}></div>
</div>
<span class="text-xs text-base-content/60"><%= entry.progress %>%</span>
<button
type="button"
phx-click="cancel_upload"
phx-value-ref={entry.ref}
phx-value-upload="logo_upload"
class="text-base-content/40 hover:text-base-content/70"
>×</button>
</div>
<%= for err <- upload_errors(@uploads.logo_upload, entry) do %>
<p class="text-error text-xs mt-1"><%= error_to_string(err) %></p>
<% end %>
<% end %>
<%= for err <- upload_errors(@uploads.logo_upload) do %>
<p class="text-error text-xs mt-1"><%= error_to_string(err) %></p>
<% end %>
<!-- Logo Size Slider -->
<%= if @logo_image do %>
<form phx-change="update_setting" phx-value-field="logo_size" class="mt-3">
<div class="flex justify-between items-center mb-2">
<span class="text-xs font-medium text-base-content/70">Logo size</span>
<span class="text-xs font-mono text-base-content/60"><%= @theme_settings.logo_size %>px</span>
</div>
<input
type="range"
min="24"
max="120"
value={@theme_settings.logo_size}
name="logo_size"
class="range range-xs range-primary w-full"
/>
</form>
<!-- SVG Recolor Toggle (only for SVG logos) -->
<%= if @logo_image.is_svg do %>
<div class="mt-3">
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={@theme_settings.logo_recolor}
phx-click="update_setting"
phx-value-field="logo_recolor"
phx-value-setting_value={if @theme_settings.logo_recolor, do: "false", else: "true"}
class="toggle toggle-sm toggle-primary"
/>
<span class="text-sm text-base-content/70">Recolour logo</span>
</label>
<%= if @theme_settings.logo_recolor do %>
<form id="logo-color-form" phx-change="update_color" phx-value-field="logo_color" phx-hook="ColorSync" class="flex items-center gap-3 mt-2">
<input
type="color"
name="value"
value={@theme_settings.logo_color}
class="w-9 h-9 rounded-lg cursor-pointer border-0 p-0"
/>
<span class="font-mono text-sm text-base-content/70"><%= @theme_settings.logo_color %></span>
</form>
<% end %>
</div>
<% end %>
<% end %>
</div>
<% end %>
</div>
<!-- Header Background Toggle -->
<div class="mb-6">
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={@theme_settings.header_background_enabled}
phx-click="update_setting"
phx-value-field="header_background_enabled"
phx-value-setting_value={if @theme_settings.header_background_enabled, do: "false", else: "true"}
class="toggle toggle-sm toggle-primary"
/>
<span class="text-sm text-base-content/80">Header background image</span>
</label>
</div>
<!-- Header Image Upload (only when enabled) -->
<%= if @theme_settings.header_background_enabled do %>
<div class="bg-base-200 rounded-xl p-4 mb-6">
<span class="block text-xs font-medium text-base-content/70 mb-2">Upload header image</span>
<form phx-change="noop" phx-submit="noop">
<label class="block bg-base-100 border border-dashed border-base-300 rounded-lg p-3 text-sm text-base-content/60 text-center cursor-pointer hover:border-base-content/40 hover:text-base-content/80 transition-all">
<span>Choose file...</span>
<.live_file_input upload={@uploads.header_upload} class="hidden" />
</label>
</form>
<%= if @header_image do %>
<div class="relative w-full h-[60px] bg-base-100 border border-base-300 rounded-lg mt-2 overflow-hidden">
<img
src={"/images/#{@header_image.id}"}
alt="Current header background"
class="w-full h-full object-cover"
/>
<button
type="button"
phx-click="remove_header"
class="absolute -top-1.5 -right-1.5 w-[18px] h-[18px] bg-base-content text-base-100 rounded-full text-xs flex items-center justify-center leading-none"
title="Remove header background"
>×</button>
</div>
<!-- Header Image Controls -->
<div class="mt-3 space-y-3">
<form phx-change="update_setting" phx-value-field="header_zoom">
<div class="flex justify-between items-center mb-2">
<span class="text-xs font-medium text-base-content/70">Zoom</span>
<span class="text-xs font-mono text-base-content/60"><%= @theme_settings.header_zoom %>%</span>
</div>
<input
type="range"
min="100"
max="200"
value={@theme_settings.header_zoom}
name="header_zoom"
class="range range-xs range-primary w-full"
/>
</form>
<form phx-change="update_setting" phx-value-field="header_position_x">
<div class="flex justify-between items-center mb-2">
<span class="text-xs font-medium text-base-content/70">Horizontal position</span>
<span class="text-xs font-mono text-base-content/60"><%= @theme_settings.header_position_x %>%</span>
</div>
<input
type="range"
min="0"
max="100"
value={@theme_settings.header_position_x}
name="header_position_x"
class="range range-xs range-primary w-full"
/>
</form>
<form phx-change="update_setting" phx-value-field="header_position_y">
<div class="flex justify-between items-center mb-2">
<span class="text-xs font-medium text-base-content/70">Vertical position</span>
<span class="text-xs font-mono text-base-content/60"><%= @theme_settings.header_position_y %>%</span>
</div>
<input
type="range"
min="0"
max="100"
value={@theme_settings.header_position_y}
name="header_position_y"
class="range range-xs range-primary w-full"
/>
</form>
</div>
<% end %>
<%= for entry <- @uploads.header_upload.entries do %>
<div class="flex items-center gap-2 mt-2">
<div class="flex-1 h-1.5 bg-base-300 rounded-full overflow-hidden">
<div class="h-full bg-primary transition-all" style={"width: #{entry.progress}%"}></div>
</div>
<span class="text-xs text-base-content/60"><%= entry.progress %>%</span>
<button
type="button"
phx-click="cancel_upload"
phx-value-ref={entry.ref}
phx-value-upload="header_upload"
class="text-base-content/40 hover:text-base-content/70"
>×</button>
</div>
<%= for err <- upload_errors(@uploads.header_upload, entry) do %>
<p class="text-error text-xs mt-1"><%= error_to_string(err) %></p>
<% end %>
<% end %>
<%= for err <- upload_errors(@uploads.header_upload) do %>
<p class="text-error text-xs mt-1"><%= error_to_string(err) %></p>
<% end %>
</div>
<% end %>
<!-- Presets Section -->
<div class="mb-6">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Start with a preset</label>
<div class="grid grid-cols-2 gap-2">
<%= for {preset_name, description} <- @presets_with_descriptions do %>
<button
type="button"
phx-click="apply_preset"
phx-value-preset={preset_name}
class={[
"p-3 rounded-lg text-left transition-all border-2",
if(@active_preset == preset_name,
do: "border-base-content bg-base-100",
else: "border-transparent bg-base-200 hover:bg-base-300"
)
]}
>
<div class="font-semibold text-sm capitalize text-base-content"><%= preset_name %></div>
<div class="text-xs text-base-content/60"><%= description %></div>
</button>
<% end %>
</div>
</div>
<!-- Accent Color (stays in essentials) -->
<div class="mb-6">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Accent colour</label>
<form id="accent-color-form" phx-change="update_color" phx-value-field="accent_color" phx-hook="ColorSync">
<div class="flex items-center gap-3">
<input
type="color"
id="accent-color-picker"
name="value"
value={@theme_settings.accent_color}
class="w-12 h-12 rounded-lg cursor-pointer border-0 p-0"
/>
<span class="font-mono text-sm text-base-content/70"><%= @theme_settings.accent_color %></span>
</div>
</form>
</div>
<!-- Customise Section (collapsible accordion using native details/summary) -->
<details class="border-t border-base-300 mt-6 pt-4 group" id="customise-section" open={@customise_open}>
<summary class="flex items-center justify-between w-full py-3 cursor-pointer list-none [&::-webkit-details-marker]:hidden" phx-click="toggle_customise">
<span class="text-sm font-semibold text-base-content/70 group-hover:text-base-content transition-colors">Customise</span>
<svg class="w-5 h-5 text-base-content/50 transition-transform group-open:rotate-180" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</summary>
<div class="pt-4">
<!-- Typography Group -->
<div class="mb-6 pb-6 border-b border-base-200">
<div class="flex items-center gap-2 mb-4">
<svg class="w-4 h-4 text-base-content/50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="4 7 4 4 20 4 20 7"></polyline>
<line x1="9" y1="20" x2="15" y2="20"></line>
<line x1="12" y1="4" x2="12" y2="20"></line>
</svg>
<span class="text-sm font-semibold text-base-content">Typography</span>
</div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Font style</label>
<div class="flex flex-wrap gap-2">
<%= for typo <- ["clean", "editorial", "modern", "classic", "friendly", "minimal"] do %>
<button
type="button"
phx-click="update_setting"
phx-value-field="typography"
phx-value-setting_value={typo}
class={[
"px-3 py-2 text-sm rounded-lg border-2 transition-all capitalize",
if(@theme_settings.typography == typo,
do: "border-base-content bg-base-100 text-base-content",
else: "border-transparent bg-base-200 hover:bg-base-300 text-base-content"
)
]}
>
<%= typo %>
</button>
<% end %>
</div>
</div>
</div>
<!-- Colours Group -->
<div class="mb-6 pb-6 border-b border-base-200">
<div class="flex items-center gap-2 mb-4">
<svg class="w-4 h-4 text-base-content/50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<circle cx="12" cy="12" r="3"></circle>
</svg>
<span class="text-sm font-semibold text-base-content">Colours</span>
</div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Colour mood</label>
<div class="flex flex-wrap gap-2">
<%= for mood <- ["warm", "neutral", "cool", "dark"] do %>
<button
type="button"
phx-click="update_setting"
phx-value-field="mood"
phx-value-setting_value={mood}
class={[
"px-3 py-2 text-sm rounded-lg border-2 transition-all capitalize",
if(@theme_settings.mood == mood,
do: "border-base-content bg-base-100 text-base-content",
else: "border-transparent bg-base-200 hover:bg-base-300 text-base-content"
)
]}
>
<%= mood %>
</button>
<% end %>
</div>
</div>
</div>
<!-- Layout Group -->
<div class="mb-6 pb-6 border-b border-base-200">
<div class="flex items-center gap-2 mb-4">
<svg class="w-4 h-4 text-base-content/50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="3" y1="9" x2="21" y2="9"></line>
<line x1="9" y1="21" x2="9" y2="9"></line>
</svg>
<span class="text-sm font-semibold text-base-content">Layout</span>
</div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Product grid</label>
<div class="flex flex-wrap 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={[
"px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.grid_columns == cols,
do: "border-base-content bg-base-100 text-base-content",
else: "border-transparent bg-base-200 hover:bg-base-300 text-base-content"
)
]}
>
<%= cols %> columns
</button>
<% end %>
</div>
</div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Density</label>
<div class="flex flex-wrap 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={[
"px-3 py-2 text-sm rounded-lg border-2 transition-all capitalize",
if(@theme_settings.density == density,
do: "border-base-content bg-base-100 text-base-content",
else: "border-transparent bg-base-200 hover:bg-base-300 text-base-content"
)
]}
>
<%= density %>
</button>
<% end %>
</div>
</div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Header layout</label>
<div class="flex flex-wrap 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={[
"px-3 py-2 text-sm rounded-lg border-2 transition-all capitalize",
if(@theme_settings.header_layout == layout,
do: "border-base-content bg-base-100 text-base-content",
else: "border-transparent bg-base-200 hover:bg-base-300 text-base-content"
)
]}
>
<%= layout %>
</button>
<% end %>
</div>
</div>
</div>
<!-- Shape Group -->
<div class="mb-4">
<div class="flex items-center gap-2 mb-4">
<svg class="w-4 h-4 text-base-content/50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
</svg>
<span class="text-sm font-semibold text-base-content">Shape</span>
</div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Corner style</label>
<div class="flex flex-wrap 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={[
"px-3 py-2 text-sm rounded-lg border-2 transition-all capitalize",
if(@theme_settings.shape == shape,
do: "border-base-content bg-base-100 text-base-content",
else: "border-transparent bg-base-200 hover:bg-base-300 text-base-content"
)
]}
>
<%= shape %>
</button>
<% end %>
</div>
</div>
</div>
</div>
</details>
<!-- Current Combination Display -->
<div class="bg-base-200 rounded-xl p-4 mt-6">
<div class="text-xs font-semibold uppercase tracking-wider text-base-content/50 mb-2">Current combination</div>
<div class="text-sm text-base-content leading-relaxed">
<%= String.capitalize(@theme_settings.mood) %> · <%= String.capitalize(@theme_settings.typography) %> · <%= String.capitalize(@theme_settings.shape) %> · <%= String.capitalize(@theme_settings.density) %> · <%= @theme_settings.grid_columns %>-up · <%= String.capitalize(@theme_settings.header_layout) %>
</div>
<div class="text-xs text-base-content/40 mt-2">One of 100,000+ possible combinations</div>
</div>
</div>
<!-- Preview Area -->
<div class="flex-1 p-6">
<div class="bg-base-100 rounded-lg shadow-xl overflow-hidden">
<div class="flex-1 p-6 flex flex-col overflow-hidden bg-base-200">
<div class="max-w-[1200px] mx-auto w-full flex flex-col flex-1 min-h-0 overflow-hidden">
<!-- Preview Page Switcher -->
<div class="bg-base-200 p-4 border-b border-base-300">
<div class="flex flex-wrap gap-2">
<%= for {page_name, label} <- [
{:home, "Home"},
{:collection, "Collection"},
{:pdp, "Product"},
{:cart, "Cart"},
{:about, "About"},
{:contact, "Contact"},
{:error, "404"}
] do %>
<button
type="button"
phx-click="change_preview_page"
phx-value-page={page_name}
class={[
"btn btn-sm",
if(@preview_page == page_name, do: "btn-primary", else: "btn-ghost")
]}
>
<%= label %>
</button>
<% end %>
<div class="flex gap-1 mb-3 bg-base-300 p-1 rounded-lg w-fit flex-shrink-0">
<%= for {page_name, label} <- [
{:home, "Home"},
{:collection, "Collection"},
{:pdp, "Product"},
{:cart, "Cart"},
{:about, "About"},
{:contact, "Contact"},
{:error, "404"}
] do %>
<button
type="button"
phx-click="change_preview_page"
phx-value-page={page_name}
class={[
"px-4 py-2 text-sm font-medium rounded transition-all",
if(@preview_page == page_name,
do: "bg-base-100 text-base-content shadow-sm",
else: "text-base-content/70 hover:text-base-content"
)
]}
>
<%= label %>
</button>
<% end %>
</div>
<!-- Browser Chrome -->
<div class="flex items-center bg-gradient-to-b from-base-300 to-base-300/80 border border-base-content/20 border-b-base-content/30 rounded-t-[10px] px-[14px] py-[10px] gap-[14px] flex-shrink-0">
<div class="flex gap-2">
<div class="w-3 h-3 rounded-full bg-[#ff5f57] border border-[#e14640]"></div>
<div class="w-3 h-3 rounded-full bg-[#ffbd2e] border border-[#dfa123]"></div>
<div class="w-3 h-3 rounded-full bg-[#28c940] border border-[#1aab29]"></div>
</div>
<div class="flex-1 flex items-center gap-2 bg-base-100 border border-base-content/20 rounded-md px-3 py-[5px]">
<svg class="w-[14px] h-[14px] text-base-content/50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
<span class="text-sm text-base-content/60 truncate"><%= @theme_settings.site_name |> String.downcase() |> String.replace(" ", "") %>.myshopify.com</span>
</div>
</div>
<!-- Preview Frame -->
<div class="preview-frame bg-white overflow-auto"
<div class="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}
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);">
data-header={@theme_settings.header_layout}>
<style>
<%= Phoenix.HTML.raw(@generated_css) %>
</style>
<%= case @preview_page do %>
<% :home -> %>
<SimpleshopThemeWeb.ThemeLive.PreviewPages.home preview_data={@preview_data} theme_settings={@theme_settings} />
<SimpleshopThemeWeb.ThemeLive.PreviewPages.home preview_data={@preview_data} theme_settings={@theme_settings} logo_image={@logo_image} header_image={@header_image} />
<% :collection -> %>
<SimpleshopThemeWeb.ThemeLive.PreviewPages.collection preview_data={@preview_data} theme_settings={@theme_settings} />
<SimpleshopThemeWeb.ThemeLive.PreviewPages.collection preview_data={@preview_data} theme_settings={@theme_settings} logo_image={@logo_image} header_image={@header_image} />
<% :pdp -> %>
<SimpleshopThemeWeb.ThemeLive.PreviewPages.pdp preview_data={@preview_data} theme_settings={@theme_settings} />
<SimpleshopThemeWeb.ThemeLive.PreviewPages.pdp preview_data={@preview_data} theme_settings={@theme_settings} logo_image={@logo_image} header_image={@header_image} />
<% :cart -> %>
<SimpleshopThemeWeb.ThemeLive.PreviewPages.cart preview_data={@preview_data} theme_settings={@theme_settings} />
<SimpleshopThemeWeb.ThemeLive.PreviewPages.cart preview_data={@preview_data} theme_settings={@theme_settings} logo_image={@logo_image} header_image={@header_image} />
<% :about -> %>
<SimpleshopThemeWeb.ThemeLive.PreviewPages.about preview_data={@preview_data} theme_settings={@theme_settings} />
<SimpleshopThemeWeb.ThemeLive.PreviewPages.about preview_data={@preview_data} theme_settings={@theme_settings} logo_image={@logo_image} header_image={@header_image} />
<% :contact -> %>
<SimpleshopThemeWeb.ThemeLive.PreviewPages.contact preview_data={@preview_data} theme_settings={@theme_settings} />
<SimpleshopThemeWeb.ThemeLive.PreviewPages.contact preview_data={@preview_data} theme_settings={@theme_settings} logo_image={@logo_image} header_image={@header_image} />
<% :error -> %>
<SimpleshopThemeWeb.ThemeLive.PreviewPages.error preview_data={@preview_data} theme_settings={@theme_settings} />
<SimpleshopThemeWeb.ThemeLive.PreviewPages.error preview_data={@preview_data} theme_settings={@theme_settings} logo_image={@logo_image} header_image={@header_image} />
<% end %>
</div>
</div>

View File

@@ -2,4 +2,103 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do
use Phoenix.Component
embed_templates "preview_pages/*"
@doc """
Renders the shop header with logo based on logo_mode setting.
"""
attr :theme_settings, :map, required: true
attr :logo_image, :map, default: nil
attr :header_image, :map, default: nil
def shop_header(assigns) do
~H"""
<header
class="shop-header"
style={"position: relative; background-color: var(--t-surface-raised); border-bottom: 1px solid var(--t-border-default); padding: 1rem 2rem; display: flex; align-items: center; #{header_justify(@theme_settings.header_layout)};"}
>
<%= if @theme_settings.header_background_enabled && @header_image do %>
<div style={header_background_style(@theme_settings, @header_image)} />
<% end %>
<%= if @theme_settings.header_layout == "centered" do %>
<nav class="shop-nav hidden md:flex" style="gap: 1.5rem; position: relative; z-index: 1;">
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">Home</a>
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">Shop</a>
</nav>
<% end %>
<div class="shop-logo" style="display: flex; align-items: center; position: relative; z-index: 1;">
<%= case @theme_settings.logo_mode do %>
<% "text-only" -> %>
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: 1.25rem; font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
<%= @theme_settings.site_name %>
</span>
<% "logo-text" -> %>
<%= if @logo_image do %>
<img
src={logo_url(@logo_image, @theme_settings)}
alt={@theme_settings.site_name}
style={"height: #{@theme_settings.logo_size}px; width: auto; margin-right: 0.5rem;"}
/>
<% end %>
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: 1.25rem; font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
<%= @theme_settings.site_name %>
</span>
<% "logo-only" -> %>
<%= if @logo_image do %>
<img
src={logo_url(@logo_image, @theme_settings)}
alt={@theme_settings.site_name}
style={"height: #{@theme_settings.logo_size}px; width: auto;"}
/>
<% else %>
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: 1.25rem; font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
<%= @theme_settings.site_name %>
</span>
<% end %>
<% _ -> %>
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: 1.25rem; font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
<%= @theme_settings.site_name %>
</span>
<% end %>
</div>
<%= if @theme_settings.header_layout != "minimal" do %>
<nav class={"shop-nav #{if @theme_settings.header_layout == "centered", do: "hidden md:flex", else: "hidden md:flex"}"} style="gap: 1.5rem; position: relative; z-index: 1;">
<%= unless @theme_settings.header_layout == "centered" do %>
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">Home</a>
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">Shop</a>
<% end %>
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">About</a>
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">Contact</a>
</nav>
<% end %>
<div class="shop-cart" style="position: relative; z-index: 1;">
<button style="color: var(--t-text-primary); background: none; border: none; cursor: pointer;">Cart (0)</button>
</div>
</header>
"""
end
defp header_justify("centered"), do: "justify-content: center; gap: 2rem;"
defp header_justify("minimal"), do: "justify-content: space-between;"
defp header_justify(_), do: "justify-content: space-between;"
defp logo_url(logo_image, %{logo_recolor: true, logo_color: color}) when logo_image.is_svg do
clean_color = String.trim_leading(color, "#")
"/images/#{logo_image.id}/recolored/#{clean_color}"
end
defp logo_url(logo_image, _), do: "/images/#{logo_image.id}"
defp header_background_style(settings, header_image) do
"position: absolute; top: 0; left: 0; right: 0; bottom: 0; " <>
"background-image: url('/images/#{header_image.id}'); " <>
"background-size: #{settings.header_zoom}%; " <>
"background-position: #{settings.header_position_x}% #{settings.header_position_y}%; " <>
"background-repeat: no-repeat; z-index: 0;"
end
end

View File

@@ -1,21 +1,6 @@
<div class="min-h-screen" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
<!-- Header -->
<header class="shop-header" style="background-color: var(--t-surface-raised); border-bottom: 1px solid var(--t-border-default); padding: 1rem 2rem; display: flex; align-items: center; justify-content: space-between;">
<div class="shop-logo">
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: 1.25rem; font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
Store Name
</span>
</div>
<nav class="shop-nav" style="gap: 1.5rem;">
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">Home</a>
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">Shop</a>
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">About</a>
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">Contact</a>
</nav>
<div class="shop-cart">
<button style="color: var(--t-text-primary); background: none; border: none; cursor: pointer;">Cart (0)</button>
</div>
</header>
<SimpleshopThemeWeb.ThemeLive.PreviewPages.shop_header theme_settings={@theme_settings} logo_image={@logo_image} header_image={@header_image} />
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<h1 class="text-4xl md:text-5xl font-bold mb-6 text-center" style="font-family: var(--t-font-heading); color: var(--t-text-primary); font-weight: var(--t-heading-weight); letter-spacing: var(--t-heading-tracking);">

View File

@@ -1,21 +1,6 @@
<div class="min-h-screen" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
<!-- Header -->
<header class="shop-header" style="background-color: var(--t-surface-raised); border-bottom: 1px solid var(--t-border-default); padding: 1rem 2rem; display: flex; align-items: center; justify-content: space-between;">
<div class="shop-logo">
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: 1.25rem; font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
Store Name
</span>
</div>
<nav class="shop-nav" style="gap: 1.5rem;">
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">Home</a>
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">Shop</a>
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">About</a>
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">Contact</a>
</nav>
<div class="shop-cart">
<button style="color: var(--t-text-primary); background: none; border: none; cursor: pointer;">Cart (0)</button>
</div>
</header>
<SimpleshopThemeWeb.ThemeLive.PreviewPages.shop_header theme_settings={@theme_settings} logo_image={@logo_image} header_image={@header_image} />
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<h1 class="text-3xl md:text-4xl font-bold mb-8" style="font-family: var(--t-font-heading); color: var(--t-text-primary); font-weight: var(--t-heading-weight); letter-spacing: var(--t-heading-tracking);">

View File

@@ -1,21 +1,6 @@
<div class="min-h-screen" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
<!-- Header -->
<header class="shop-header" style="background-color: var(--t-surface-raised); border-bottom: 1px solid var(--t-border-default); padding: 1rem 2rem; display: flex; align-items: center; justify-content: space-between;">
<div class="shop-logo">
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: 1.25rem; font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
Store Name
</span>
</div>
<nav class="shop-nav" style="gap: 1.5rem;">
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">Home</a>
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">Shop</a>
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">About</a>
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">Contact</a>
</nav>
<div class="shop-cart">
<button style="color: var(--t-text-primary); background: none; border: none; cursor: pointer;">Cart (0)</button>
</div>
</header>
<SimpleshopThemeWeb.ThemeLive.PreviewPages.shop_header theme_settings={@theme_settings} logo_image={@logo_image} header_image={@header_image} />
<!-- Page Header -->
<div class="border-b" style="background-color: var(--t-surface-raised); border-color: var(--t-border-default);">

View File

@@ -1,21 +1,6 @@
<div class="min-h-screen" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
<!-- Header -->
<header class="shop-header" style="background-color: var(--t-surface-raised); border-bottom: 1px solid var(--t-border-default); padding: 1rem 2rem; display: flex; align-items: center; justify-content: space-between;">
<div class="shop-logo">
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: 1.25rem; font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
Store Name
</span>
</div>
<nav class="shop-nav" style="gap: 1.5rem;">
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">Home</a>
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">Shop</a>
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">About</a>
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">Contact</a>
</nav>
<div class="shop-cart">
<button style="color: var(--t-text-primary); background: none; border: none; cursor: pointer;">Cart (0)</button>
</div>
</header>
<SimpleshopThemeWeb.ThemeLive.PreviewPages.shop_header theme_settings={@theme_settings} logo_image={@logo_image} header_image={@header_image} />
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<h1 class="text-4xl md:text-5xl font-bold mb-6 text-center" style="font-family: var(--t-font-heading); color: var(--t-text-primary); font-weight: var(--t-heading-weight); letter-spacing: var(--t-heading-tracking);">

View File

@@ -1,21 +1,6 @@
<div class="min-h-screen" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
<!-- Header -->
<header class="shop-header" style="background-color: var(--t-surface-raised); border-bottom: 1px solid var(--t-border-default); padding: 1rem 2rem; display: flex; align-items: center; justify-content: space-between;">
<div class="shop-logo">
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: 1.25rem; font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
Store Name
</span>
</div>
<nav class="shop-nav" style="gap: 1.5rem;">
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">Home</a>
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">Shop</a>
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">About</a>
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">Contact</a>
</nav>
<div class="shop-cart">
<button style="color: var(--t-text-primary); background: none; border: none; cursor: pointer;">Cart (0)</button>
</div>
</header>
<SimpleshopThemeWeb.ThemeLive.PreviewPages.shop_header theme_settings={@theme_settings} logo_image={@logo_image} header_image={@header_image} />
<div class="flex items-center justify-center" style="min-height: calc(100vh - 4rem);">
<div class="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-16 text-center">

View File

@@ -1,21 +1,6 @@
<div class="min-h-screen" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
<!-- Header -->
<header class="shop-header" style="background-color: var(--t-surface-raised); border-bottom: 1px solid var(--t-border-default); padding: 1rem 2rem; display: flex; align-items: center; justify-content: space-between;">
<div class="shop-logo">
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: 1.25rem; font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
Store Name
</span>
</div>
<nav class="shop-nav" style="gap: 1.5rem;">
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">Home</a>
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">Shop</a>
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">About</a>
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">Contact</a>
</nav>
<div class="shop-cart">
<button style="color: var(--t-text-primary); background: none; border: none; cursor: pointer;">Cart (0)</button>
</div>
</header>
<SimpleshopThemeWeb.ThemeLive.PreviewPages.shop_header theme_settings={@theme_settings} logo_image={@logo_image} header_image={@header_image} />
<!-- Hero Section -->
<div class="relative" style="background-color: var(--t-surface-raised);">

View File

@@ -3,22 +3,7 @@
%>
<div class="min-h-screen" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
<!-- Header -->
<header class="shop-header" style="background-color: var(--t-surface-raised); border-bottom: 1px solid var(--t-border-default); padding: 1rem 2rem; display: flex; align-items: center; justify-content: space-between;">
<div class="shop-logo">
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: 1.25rem; font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
Store Name
</span>
</div>
<nav class="shop-nav" style="gap: 1.5rem;">
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">Home</a>
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">Shop</a>
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">About</a>
<a href="#" style="color: var(--t-text-secondary); text-decoration: none;">Contact</a>
</nav>
<div class="shop-cart">
<button style="color: var(--t-text-primary); background: none; border: none; cursor: pointer;">Cart (0)</button>
</div>
</header>
<SimpleshopThemeWeb.ThemeLive.PreviewPages.shop_header theme_settings={@theme_settings} logo_image={@logo_image} header_image={@header_image} />
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Breadcrumb -->

View File

@@ -23,6 +23,15 @@ defmodule SimpleshopThemeWeb.Router do
get "/", PageController, :home
end
# Image serving routes (public, no auth required)
scope "/images", SimpleshopThemeWeb do
pipe_through :browser
get "/:id", ImageController, :show
get "/:id/thumbnail", ImageController, :thumbnail
get "/:id/recolored/:color", ImageController, :recolored_svg
end
# Other scopes may use custom stacks.
# scope "/api", SimpleshopThemeWeb do
# pipe_through :api