consolidate shop pages into unified LiveView for editor state persistence
All checks were successful
deploy / deploy (push) Successful in 1m27s
All checks were successful
deploy / deploy (push) Successful in 1m27s
Replace individual shop LiveViews with a single Shop.Page that dispatches to page modules based on live_action. This enables patch navigation between pages, preserving socket state (including editor state) across transitions. Changes: - Add Shop.Page unified LiveView with handle_params dispatch - Extract page logic into Shop.Pages.* modules (Home, Product, Collection, etc.) - Update router to use Shop.Page with live_action for all shop routes - Change navigate= to patch= in shop component links - Add maybe_sync_editing_blocks to reload editor state when page changes - Track editor_page_slug to detect cross-page navigation while editing - Fix picture element height when hover image disabled - Extract ThemeEditor components for shared use Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -171,7 +171,7 @@ defmodule BerrypodWeb.ShopComponents.Base do
|
||||
def shop_link_button(assigns) do
|
||||
~H"""
|
||||
<.link
|
||||
navigate={@href}
|
||||
patch={@href}
|
||||
class={["themed-button", @class]}
|
||||
>
|
||||
{render_slot(@inner_block)}
|
||||
@@ -203,7 +203,7 @@ defmodule BerrypodWeb.ShopComponents.Base do
|
||||
def shop_link_outline(assigns) do
|
||||
~H"""
|
||||
<.link
|
||||
navigate={@href}
|
||||
patch={@href}
|
||||
class={["themed-button-outline", @class]}
|
||||
>
|
||||
{render_slot(@inner_block)}
|
||||
|
||||
@@ -178,7 +178,7 @@ defmodule BerrypodWeb.ShopComponents.Cart do
|
||||
>
|
||||
<%= if @mode != :preview do %>
|
||||
<.link
|
||||
navigate={"/products/#{@item.product_id}"}
|
||||
patch={"/products/#{@item.product_id}"}
|
||||
class={["cart-item-image", !@item.image && "cart-item-image--empty"]}
|
||||
data-size={if @size == :compact, do: "compact"}
|
||||
style={if @item.image, do: "background-image: url('#{@item.image}');"}
|
||||
@@ -197,7 +197,7 @@ defmodule BerrypodWeb.ShopComponents.Cart do
|
||||
<h3 class="cart-item-name" data-size={if @size == :compact, do: "compact"}>
|
||||
<%= if @mode != :preview do %>
|
||||
<.link
|
||||
navigate={"/products/#{@item.product_id}"}
|
||||
patch={"/products/#{@item.product_id}"}
|
||||
class="cart-item-name-link"
|
||||
>
|
||||
{@item.name}
|
||||
@@ -296,7 +296,7 @@ defmodule BerrypodWeb.ShopComponents.Cart do
|
||||
</button>
|
||||
<% else %>
|
||||
<.link
|
||||
navigate="/collections/all"
|
||||
patch="/collections/all"
|
||||
class="cart-continue-link"
|
||||
>
|
||||
Continue shopping
|
||||
|
||||
@@ -266,7 +266,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
</a>
|
||||
<% else %>
|
||||
<.link
|
||||
navigate={@href}
|
||||
patch={@href}
|
||||
class="mobile-nav-link"
|
||||
aria-current={if @is_current, do: "page", else: nil}
|
||||
>
|
||||
@@ -484,7 +484,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
aria-selected="false"
|
||||
>
|
||||
<.link
|
||||
navigate={"/products/#{item.product.slug || item.product.id}"}
|
||||
patch={"/products/#{item.product.slug || item.product.id}"}
|
||||
class="search-result"
|
||||
phx-click={Phoenix.LiveView.JS.dispatch("close-search", to: "#search-modal")}
|
||||
>
|
||||
@@ -588,7 +588,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
</a>
|
||||
<% else %>
|
||||
<.link
|
||||
navigate={item["href"]}
|
||||
patch={item["href"]}
|
||||
class="mobile-nav-link"
|
||||
aria-current={@active_page in (item["active_slugs"] || [item["slug"]]) && "page"}
|
||||
phx-click={Phoenix.LiveView.JS.dispatch("close-mobile-nav", to: "#mobile-nav-drawer")}
|
||||
@@ -615,7 +615,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
</a>
|
||||
<% else %>
|
||||
<.link
|
||||
navigate={"/collections/#{category.slug}"}
|
||||
patch={"/collections/#{category.slug}"}
|
||||
class="mobile-nav-link"
|
||||
phx-click={
|
||||
Phoenix.LiveView.JS.dispatch("close-mobile-nav", to: "#mobile-nav-drawer")
|
||||
@@ -700,7 +700,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
<% else %>
|
||||
<li>
|
||||
<.link
|
||||
navigate="/collections/all"
|
||||
patch="/collections/all"
|
||||
class="footer-link"
|
||||
>
|
||||
All products
|
||||
@@ -709,7 +709,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
<%= for category <- @categories do %>
|
||||
<li>
|
||||
<.link
|
||||
navigate={"/collections/#{category.slug}"}
|
||||
patch={"/collections/#{category.slug}"}
|
||||
class="footer-link"
|
||||
>
|
||||
{category.name}
|
||||
@@ -735,7 +735,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
{item["label"]}
|
||||
</a>
|
||||
<% else %>
|
||||
<.link navigate={item["href"]} class="footer-link">
|
||||
<.link patch={item["href"]} class="footer-link">
|
||||
{item["label"]}
|
||||
</.link>
|
||||
<% end %>
|
||||
@@ -929,7 +929,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
/>
|
||||
</a>
|
||||
<% else %>
|
||||
<.link navigate="/" class="shop-logo-link">
|
||||
<.link patch="/" class="shop-logo-link">
|
||||
<.logo_inner
|
||||
theme_settings={@theme_settings}
|
||||
site_name={@site_name}
|
||||
@@ -1015,7 +1015,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
{@label}
|
||||
</a>
|
||||
<% else %>
|
||||
<.link navigate={@href} class="nav-link">
|
||||
<.link patch={@href} class="nav-link">
|
||||
{@label}
|
||||
</.link>
|
||||
<% end %>
|
||||
|
||||
@@ -157,7 +157,7 @@ defmodule BerrypodWeb.ShopComponents.Product do
|
||||
</p>
|
||||
<% else %>
|
||||
<.link
|
||||
navigate={"/collections/#{Slug.slugify(@product.category)}"}
|
||||
patch={"/collections/#{Slug.slugify(@product.category)}"}
|
||||
class="product-card-category"
|
||||
>
|
||||
{@product.category}
|
||||
@@ -177,7 +177,7 @@ defmodule BerrypodWeb.ShopComponents.Product do
|
||||
</a>
|
||||
<% else %>
|
||||
<.link
|
||||
navigate={"/products/#{Map.get(@product, :slug) || Map.get(@product, :id)}"}
|
||||
patch={"/products/#{Map.get(@product, :slug) || Map.get(@product, :id)}"}
|
||||
class="stretched-link"
|
||||
>
|
||||
{@product.title}
|
||||
@@ -205,7 +205,7 @@ defmodule BerrypodWeb.ShopComponents.Product do
|
||||
defp product_card_image_wrap(assigns) do
|
||||
~H"""
|
||||
<%= if @href do %>
|
||||
<.link navigate={@href} class="product-card-image-wrap" tabindex="-1" aria-hidden="true">
|
||||
<.link patch={@href} class="product-card-image-wrap" tabindex="-1" aria-hidden="true">
|
||||
{render_slot(@inner_block)}
|
||||
</.link>
|
||||
<% else %>
|
||||
@@ -571,7 +571,7 @@ defmodule BerrypodWeb.ShopComponents.Product do
|
||||
</button>
|
||||
<% else %>
|
||||
<.link
|
||||
navigate={@href || "/"}
|
||||
patch={@href || "/"}
|
||||
class={@cta_class}
|
||||
>
|
||||
{@text}
|
||||
@@ -627,7 +627,7 @@ defmodule BerrypodWeb.ShopComponents.Product do
|
||||
</a>
|
||||
<% else %>
|
||||
<.link
|
||||
navigate={"/collections/#{category.slug}"}
|
||||
patch={"/collections/#{category.slug}"}
|
||||
class="category-card"
|
||||
>
|
||||
<div
|
||||
@@ -713,7 +713,7 @@ defmodule BerrypodWeb.ShopComponents.Product do
|
||||
</button>
|
||||
<% else %>
|
||||
<.link
|
||||
navigate={@cta_href}
|
||||
patch={@cta_href}
|
||||
class="outline-button"
|
||||
>
|
||||
{@cta_text}
|
||||
@@ -826,7 +826,7 @@ defmodule BerrypodWeb.ShopComponents.Product do
|
||||
</a>
|
||||
<% else %>
|
||||
<.link
|
||||
navigate={@link_href || "/"}
|
||||
patch={@link_href || "/"}
|
||||
class="accent-link"
|
||||
>
|
||||
{@link_text}
|
||||
@@ -960,7 +960,7 @@ defmodule BerrypodWeb.ShopComponents.Product do
|
||||
{item.label}
|
||||
</a>
|
||||
<% else %>
|
||||
<.link navigate={item.href || "/"}>{item.label}</.link>
|
||||
<.link patch={item.href || "/"}>{item.label}</.link>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
@@ -1749,7 +1749,7 @@ defmodule BerrypodWeb.ShopComponents.Product do
|
||||
<div class="shop-pagination-buttons">
|
||||
<.link
|
||||
:if={@page.page > 1}
|
||||
navigate={pagination_url(@base_path, @page.page - 1, @params)}
|
||||
patch={pagination_url(@base_path, @page.page - 1, @params)}
|
||||
class="shop-pagination-btn"
|
||||
aria-label="Previous page"
|
||||
>
|
||||
@@ -1762,7 +1762,7 @@ defmodule BerrypodWeb.ShopComponents.Product do
|
||||
<span class="shop-pagination-ellipsis" aria-hidden="true">…</span>
|
||||
<% n -> %>
|
||||
<.link
|
||||
navigate={pagination_url(@base_path, n, @params)}
|
||||
patch={pagination_url(@base_path, n, @params)}
|
||||
aria-label={"Page #{n}"}
|
||||
aria-current={n == @page.page && "page"}
|
||||
class={["shop-pagination-btn", n == @page.page && "shop-pagination-btn-active"]}
|
||||
@@ -1774,7 +1774,7 @@ defmodule BerrypodWeb.ShopComponents.Product do
|
||||
|
||||
<.link
|
||||
:if={@page.page < @page.total_pages}
|
||||
navigate={pagination_url(@base_path, @page.page + 1, @params)}
|
||||
patch={pagination_url(@base_path, @page.page + 1, @params)}
|
||||
class="shop-pagination-btn"
|
||||
aria-label="Next page"
|
||||
>
|
||||
|
||||
731
lib/berrypod_web/components/shop_components/theme_editor.ex
Normal file
731
lib/berrypod_web/components/shop_components/theme_editor.ex
Normal file
@@ -0,0 +1,731 @@
|
||||
defmodule BerrypodWeb.ShopComponents.ThemeEditor do
|
||||
@moduledoc """
|
||||
Shared theme editor components used in both:
|
||||
- Admin theme page (`/admin/theme`)
|
||||
- On-site editor panel (page editor Theme tab)
|
||||
|
||||
Components render settings controls that emit standard events:
|
||||
- `update_setting` / `theme_update_setting` (phx-click/phx-change)
|
||||
- `toggle_setting` / `theme_toggle_setting` (phx-click)
|
||||
- `apply_preset` / `theme_apply_preset` (phx-click)
|
||||
- `update_color` / `theme_update_color` (phx-change)
|
||||
|
||||
The event prefix is controlled by `@event_prefix`:
|
||||
- `""` (default) for admin context
|
||||
- `"theme_"` for on-site editor context
|
||||
"""
|
||||
|
||||
use Phoenix.Component
|
||||
|
||||
# ── Quick Settings ─────────────────────────────────────────────────
|
||||
# These are the core settings shown in both compact and full modes.
|
||||
|
||||
@doc """
|
||||
Renders the shop name input field.
|
||||
"""
|
||||
attr :site_name, :string, required: true
|
||||
attr :event_prefix, :string, default: ""
|
||||
|
||||
def shop_name_input(assigns) do
|
||||
~H"""
|
||||
<div class="theme-section">
|
||||
<label class="theme-section-label">Shop name</label>
|
||||
<form phx-change={@event_prefix <> "update_setting"} phx-value-field="site_name">
|
||||
<input
|
||||
type="text"
|
||||
name="site_name"
|
||||
value={@site_name}
|
||||
placeholder="Your shop name"
|
||||
class="admin-input"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders the preset grid for quick theme switching.
|
||||
"""
|
||||
attr :presets, :list, required: true
|
||||
attr :active_preset, :atom, default: nil
|
||||
attr :event_prefix, :string, default: ""
|
||||
attr :label, :string, default: "Preset"
|
||||
|
||||
def preset_grid(assigns) do
|
||||
~H"""
|
||||
<div class="theme-section">
|
||||
<label class="theme-section-label">{@label}</label>
|
||||
<div class="theme-presets">
|
||||
<%= for {preset_name, description} <- @presets do %>
|
||||
<button
|
||||
type="button"
|
||||
phx-click={@event_prefix <> "apply_preset"}
|
||||
phx-value-preset={preset_name}
|
||||
class={["theme-preset", @active_preset == preset_name && "theme-preset-active"]}
|
||||
>
|
||||
<div class="theme-preset-name">{preset_name}</div>
|
||||
<div class="theme-preset-desc">{description}</div>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders the colour mood chip selector.
|
||||
"""
|
||||
attr :theme_settings, :map, required: true
|
||||
attr :event_prefix, :string, default: ""
|
||||
|
||||
def mood_chips(assigns) do
|
||||
~H"""
|
||||
<div class="theme-section">
|
||||
<label class="theme-section-label">Colour mood</label>
|
||||
<div class="theme-chips">
|
||||
<%= for mood <- ["warm", "neutral", "cool", "dark"] do %>
|
||||
<button
|
||||
type="button"
|
||||
phx-click={@event_prefix <> "update_setting"}
|
||||
phx-value-field="mood"
|
||||
phx-value-setting_value={mood}
|
||||
class={["theme-chip", @theme_settings.mood == mood && "theme-chip-active"]}
|
||||
>
|
||||
{mood}
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders the font style chip selector.
|
||||
"""
|
||||
attr :theme_settings, :map, required: true
|
||||
attr :event_prefix, :string, default: ""
|
||||
|
||||
def typography_chips(assigns) do
|
||||
~H"""
|
||||
<div class="theme-section">
|
||||
<label class="theme-section-label">Font style</label>
|
||||
<div class="theme-chips">
|
||||
<%= for typo <- ["clean", "editorial", "modern", "classic", "friendly", "minimal"] do %>
|
||||
<button
|
||||
type="button"
|
||||
phx-click={@event_prefix <> "update_setting"}
|
||||
phx-value-field="typography"
|
||||
phx-value-setting_value={typo}
|
||||
class={["theme-chip", @theme_settings.typography == typo && "theme-chip-active"]}
|
||||
>
|
||||
{typo}
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders the corner style chip selector.
|
||||
"""
|
||||
attr :theme_settings, :map, required: true
|
||||
attr :event_prefix, :string, default: ""
|
||||
|
||||
def shape_chips(assigns) do
|
||||
~H"""
|
||||
<div class="theme-section">
|
||||
<label class="theme-section-label">Corner style</label>
|
||||
<div class="theme-chips">
|
||||
<%= for shape <- ["sharp", "soft", "round", "pill"] do %>
|
||||
<button
|
||||
type="button"
|
||||
phx-click={@event_prefix <> "update_setting"}
|
||||
phx-value-field="shape"
|
||||
phx-value-setting_value={shape}
|
||||
class={["theme-chip", @theme_settings.shape == shape && "theme-chip-active"]}
|
||||
>
|
||||
{shape}
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
# ── Accent Colors ──────────────────────────────────────────────────
|
||||
|
||||
@doc """
|
||||
Renders an accent colour picker with ColorSync hook.
|
||||
"""
|
||||
attr :field, :string, required: true
|
||||
attr :label, :string, required: true
|
||||
attr :value, :string, required: true
|
||||
attr :event_prefix, :string, default: ""
|
||||
|
||||
def color_picker(assigns) do
|
||||
~H"""
|
||||
<div class="theme-section">
|
||||
<label class="theme-section-label">{@label}</label>
|
||||
<form
|
||||
id={"#{@field}-color-form"}
|
||||
phx-change={@event_prefix <> "update_color"}
|
||||
phx-value-field={@field}
|
||||
phx-hook="ColorSync"
|
||||
>
|
||||
<div class="theme-color-row">
|
||||
<input
|
||||
type="color"
|
||||
id={"#{@field}-color-picker"}
|
||||
name="value"
|
||||
value={@value}
|
||||
class="theme-color-swatch"
|
||||
/>
|
||||
<span class="theme-color-value">{@value}</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
# ── Compact Mode ───────────────────────────────────────────────────
|
||||
# Quick settings panel for on-site editor.
|
||||
|
||||
@doc """
|
||||
Renders the on-site theme editor panel.
|
||||
|
||||
Shows all theme settings including presets, colours, and the full
|
||||
customise accordion with advanced options.
|
||||
"""
|
||||
attr :theme_settings, :map, required: true
|
||||
attr :active_preset, :atom, default: nil
|
||||
attr :presets, :list, default: []
|
||||
attr :site_name, :string, default: ""
|
||||
attr :customise_open, :boolean, default: false
|
||||
attr :event_prefix, :string, default: "theme_"
|
||||
|
||||
def compact_editor(assigns) do
|
||||
~H"""
|
||||
<div class="editor-theme-content">
|
||||
<%= if @theme_settings do %>
|
||||
<.shop_name_input site_name={@site_name} event_prefix={@event_prefix} />
|
||||
<.preset_grid presets={@presets} active_preset={@active_preset} event_prefix={@event_prefix} />
|
||||
|
||||
<.color_picker
|
||||
field="accent_color"
|
||||
label="Accent colour"
|
||||
value={@theme_settings.accent_color}
|
||||
event_prefix={@event_prefix}
|
||||
/>
|
||||
<.color_picker
|
||||
field="secondary_accent_color"
|
||||
label="Hover colour"
|
||||
value={@theme_settings.secondary_accent_color}
|
||||
event_prefix={@event_prefix}
|
||||
/>
|
||||
<.color_picker
|
||||
field="sale_color"
|
||||
label="Sale colour"
|
||||
value={@theme_settings.sale_color}
|
||||
event_prefix={@event_prefix}
|
||||
/>
|
||||
|
||||
<.customise_accordion
|
||||
theme_settings={@theme_settings}
|
||||
customise_open={@customise_open}
|
||||
event_prefix={@event_prefix}
|
||||
/>
|
||||
|
||||
<div class="theme-section">
|
||||
<p class="admin-text-secondary">
|
||||
For logo and header image uploads, <a href="/admin/theme" class="admin-link">visit the full theme editor</a>.
|
||||
</p>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="admin-text-secondary">Loading theme settings...</p>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
# ── Full Customise Accordion ───────────────────────────────────────
|
||||
# Advanced settings groups for admin theme page.
|
||||
|
||||
@doc """
|
||||
Renders the customise accordion with all advanced settings groups.
|
||||
"""
|
||||
attr :theme_settings, :map, required: true
|
||||
attr :customise_open, :boolean, default: false
|
||||
attr :event_prefix, :string, default: ""
|
||||
|
||||
def customise_accordion(assigns) do
|
||||
~H"""
|
||||
<details class="theme-customise" id="customise-section" open={@customise_open}>
|
||||
<summary class="theme-customise-summary" phx-click={@event_prefix <> "toggle_customise"}>
|
||||
<span class="theme-customise-label">Customise</span>
|
||||
<svg
|
||||
class="theme-customise-chevron"
|
||||
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="theme-customise-body">
|
||||
<.typography_group theme_settings={@theme_settings} event_prefix={@event_prefix} />
|
||||
<.colours_group theme_settings={@theme_settings} event_prefix={@event_prefix} />
|
||||
<.layout_group theme_settings={@theme_settings} event_prefix={@event_prefix} />
|
||||
<.shape_group theme_settings={@theme_settings} event_prefix={@event_prefix} />
|
||||
<.products_group theme_settings={@theme_settings} event_prefix={@event_prefix} />
|
||||
<.product_page_group theme_settings={@theme_settings} event_prefix={@event_prefix} />
|
||||
</div>
|
||||
</details>
|
||||
"""
|
||||
end
|
||||
|
||||
# ── Setting Groups ─────────────────────────────────────────────────
|
||||
|
||||
attr :theme_settings, :map, required: true
|
||||
attr :event_prefix, :string, default: ""
|
||||
|
||||
defp typography_group(assigns) do
|
||||
~H"""
|
||||
<div class="theme-group">
|
||||
<div class="theme-group-header">
|
||||
<svg
|
||||
class="theme-group-icon"
|
||||
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="theme-group-title">Typography</span>
|
||||
</div>
|
||||
|
||||
<div class="theme-field">
|
||||
<label class="theme-section-label">Font style</label>
|
||||
<div class="theme-chips">
|
||||
<%= for typo <- ["clean", "editorial", "modern", "classic", "friendly", "minimal"] do %>
|
||||
<button
|
||||
type="button"
|
||||
phx-click={@event_prefix <> "update_setting"}
|
||||
phx-value-field="typography"
|
||||
phx-value-setting_value={typo}
|
||||
class={["theme-chip", @theme_settings.typography == typo && "theme-chip-active"]}
|
||||
>
|
||||
{typo}
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="theme-field">
|
||||
<label class="theme-section-label">Font size</label>
|
||||
<div class="theme-chips">
|
||||
<%= for {value, label} <- [{"small", "Small"}, {"medium", "Medium"}, {"large", "Large"}] do %>
|
||||
<button
|
||||
type="button"
|
||||
phx-click={@event_prefix <> "update_setting"}
|
||||
phx-value-field="font_size"
|
||||
phx-value-setting_value={value}
|
||||
class={["theme-chip", @theme_settings.font_size == value && "theme-chip-active"]}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="theme-field">
|
||||
<label class="theme-section-label">Heading weight</label>
|
||||
<div class="theme-chips">
|
||||
<%= for {value, label} <- [{"regular", "Regular"}, {"medium", "Medium"}, {"bold", "Bold"}] do %>
|
||||
<button
|
||||
type="button"
|
||||
phx-click={@event_prefix <> "update_setting"}
|
||||
phx-value-field="heading_weight"
|
||||
phx-value-setting_value={value}
|
||||
class={["theme-chip", @theme_settings.heading_weight == value && "theme-chip-active"]}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :theme_settings, :map, required: true
|
||||
attr :event_prefix, :string, default: ""
|
||||
|
||||
defp colours_group(assigns) do
|
||||
~H"""
|
||||
<div class="theme-group">
|
||||
<div class="theme-group-header">
|
||||
<svg
|
||||
class="theme-group-icon"
|
||||
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="theme-group-title">Colours</span>
|
||||
</div>
|
||||
|
||||
<div class="theme-field">
|
||||
<label class="theme-section-label">Colour mood</label>
|
||||
<div class="theme-chips">
|
||||
<%= for mood <- ["warm", "neutral", "cool", "dark"] do %>
|
||||
<button
|
||||
type="button"
|
||||
phx-click={@event_prefix <> "update_setting"}
|
||||
phx-value-field="mood"
|
||||
phx-value-setting_value={mood}
|
||||
class={["theme-chip", @theme_settings.mood == mood && "theme-chip-active"]}
|
||||
>
|
||||
{mood}
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :theme_settings, :map, required: true
|
||||
attr :event_prefix, :string, default: ""
|
||||
|
||||
defp layout_group(assigns) do
|
||||
~H"""
|
||||
<div class="theme-group">
|
||||
<div class="theme-group-header">
|
||||
<svg
|
||||
class="theme-group-icon"
|
||||
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="theme-group-title">Layout</span>
|
||||
</div>
|
||||
|
||||
<div class="theme-field">
|
||||
<label class="theme-section-label">Product grid</label>
|
||||
<div class="theme-chips">
|
||||
<%= for cols <- ["2", "3", "4"] do %>
|
||||
<button
|
||||
type="button"
|
||||
phx-click={@event_prefix <> "update_setting"}
|
||||
phx-value-field="grid_columns"
|
||||
phx-value-setting_value={cols}
|
||||
class={["theme-chip", @theme_settings.grid_columns == cols && "theme-chip-active"]}
|
||||
>
|
||||
{cols} columns
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="theme-field">
|
||||
<label class="theme-section-label">Density</label>
|
||||
<div class="theme-chips">
|
||||
<%= for density <- ["spacious", "balanced", "compact"] do %>
|
||||
<button
|
||||
type="button"
|
||||
phx-click={@event_prefix <> "update_setting"}
|
||||
phx-value-field="density"
|
||||
phx-value-setting_value={density}
|
||||
class={["theme-chip", @theme_settings.density == density && "theme-chip-active"]}
|
||||
>
|
||||
{density}
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="theme-field">
|
||||
<label class="theme-section-label">Header layout</label>
|
||||
<div class="theme-chips">
|
||||
<%= for layout <- ["standard", "centered", "left"] do %>
|
||||
<button
|
||||
type="button"
|
||||
phx-click={@event_prefix <> "update_setting"}
|
||||
phx-value-field="header_layout"
|
||||
phx-value-setting_value={layout}
|
||||
class={["theme-chip", @theme_settings.header_layout == layout && "theme-chip-active"]}
|
||||
>
|
||||
{layout}
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<.toggle_field
|
||||
field="announcement_bar"
|
||||
label="Announcement bar"
|
||||
checked={@theme_settings.announcement_bar}
|
||||
event_prefix={@event_prefix}
|
||||
/>
|
||||
|
||||
<.toggle_field
|
||||
field="sticky_header"
|
||||
label="Sticky header"
|
||||
checked={@theme_settings.sticky_header}
|
||||
event_prefix={@event_prefix}
|
||||
/>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :theme_settings, :map, required: true
|
||||
attr :event_prefix, :string, default: ""
|
||||
|
||||
defp shape_group(assigns) do
|
||||
~H"""
|
||||
<div class="theme-group-flush">
|
||||
<div class="theme-group-header">
|
||||
<svg
|
||||
class="theme-group-icon"
|
||||
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="theme-group-title">Shape</span>
|
||||
</div>
|
||||
|
||||
<div class="theme-field">
|
||||
<label class="theme-section-label">Corner style</label>
|
||||
<div class="theme-chips">
|
||||
<%= for shape <- ["sharp", "soft", "round", "pill"] do %>
|
||||
<button
|
||||
type="button"
|
||||
phx-click={@event_prefix <> "update_setting"}
|
||||
phx-value-field="shape"
|
||||
phx-value-setting_value={shape}
|
||||
class={["theme-chip", @theme_settings.shape == shape && "theme-chip-active"]}
|
||||
>
|
||||
{shape}
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="theme-field">
|
||||
<label class="theme-section-label">Card shadow</label>
|
||||
<div class="theme-chips">
|
||||
<%= for {value, label} <- [{"none", "None"}, {"sm", "Subtle"}, {"md", "Medium"}, {"lg", "Strong"}] do %>
|
||||
<button
|
||||
type="button"
|
||||
phx-click={@event_prefix <> "update_setting"}
|
||||
phx-value-field="card_shadow"
|
||||
phx-value-setting_value={value}
|
||||
class={["theme-chip", @theme_settings.card_shadow == value && "theme-chip-active"]}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="theme-field">
|
||||
<label class="theme-section-label">Button style</label>
|
||||
<div class="theme-chips">
|
||||
<%= for {value, label} <- [{"filled", "Filled"}, {"outline", "Outline"}, {"soft", "Soft"}] do %>
|
||||
<button
|
||||
type="button"
|
||||
phx-click={@event_prefix <> "update_setting"}
|
||||
phx-value-field="button_style"
|
||||
phx-value-setting_value={value}
|
||||
class={["theme-chip", @theme_settings.button_style == value && "theme-chip-active"]}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :theme_settings, :map, required: true
|
||||
attr :event_prefix, :string, default: ""
|
||||
|
||||
defp products_group(assigns) do
|
||||
~H"""
|
||||
<div class="theme-group">
|
||||
<div class="theme-group-header">
|
||||
<svg
|
||||
class="theme-group-icon"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<rect x="3" y="3" width="7" height="7"></rect>
|
||||
<rect x="14" y="3" width="7" height="7"></rect>
|
||||
<rect x="14" y="14" width="7" height="7"></rect>
|
||||
<rect x="3" y="14" width="7" height="7"></rect>
|
||||
</svg>
|
||||
<span class="theme-group-title">Products</span>
|
||||
</div>
|
||||
|
||||
<div class="theme-field">
|
||||
<label class="theme-section-label">Content width</label>
|
||||
<div class="theme-chips">
|
||||
<%= for width <- ["contained", "wide", "full"] do %>
|
||||
<button
|
||||
type="button"
|
||||
phx-click={@event_prefix <> "update_setting"}
|
||||
phx-value-field="layout_width"
|
||||
phx-value-setting_value={width}
|
||||
class={["theme-chip", @theme_settings.layout_width == width && "theme-chip-active"]}
|
||||
>
|
||||
{width}
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="theme-field">
|
||||
<label class="theme-section-label">Image aspect ratio</label>
|
||||
<div class="theme-chips">
|
||||
<%= for {value, label} <- [{"square", "Square"}, {"portrait", "Portrait"}, {"landscape", "Landscape"}] do %>
|
||||
<button
|
||||
type="button"
|
||||
phx-click={@event_prefix <> "update_setting"}
|
||||
phx-value-field="image_aspect_ratio"
|
||||
phx-value-setting_value={value}
|
||||
class={[
|
||||
"theme-chip",
|
||||
@theme_settings.image_aspect_ratio == value && "theme-chip-active"
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="theme-field">
|
||||
<label class="theme-section-label">Product text alignment</label>
|
||||
<div class="theme-chips">
|
||||
<%= for {value, label} <- [{"left", "Left"}, {"center", "Centre"}] do %>
|
||||
<button
|
||||
type="button"
|
||||
phx-click={@event_prefix <> "update_setting"}
|
||||
phx-value-field="product_text_align"
|
||||
phx-value-setting_value={value}
|
||||
class={[
|
||||
"theme-chip",
|
||||
@theme_settings.product_text_align == value && "theme-chip-active"
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<.toggle_field
|
||||
field="hover_image"
|
||||
label="Second image on hover"
|
||||
checked={@theme_settings.hover_image}
|
||||
event_prefix={@event_prefix}
|
||||
/>
|
||||
|
||||
<.toggle_field
|
||||
field="show_prices"
|
||||
label="Show prices"
|
||||
checked={@theme_settings.show_prices}
|
||||
event_prefix={@event_prefix}
|
||||
/>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :theme_settings, :map, required: true
|
||||
attr :event_prefix, :string, default: ""
|
||||
|
||||
defp product_page_group(assigns) do
|
||||
~H"""
|
||||
<div class="theme-group-flush">
|
||||
<div class="theme-group-header">
|
||||
<svg
|
||||
class="theme-group-icon"
|
||||
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>
|
||||
</svg>
|
||||
<span class="theme-group-title">Product page</span>
|
||||
</div>
|
||||
|
||||
<.toggle_field
|
||||
field="pdp_trust_badges"
|
||||
label="Trust badges"
|
||||
checked={@theme_settings.pdp_trust_badges}
|
||||
event_prefix={@event_prefix}
|
||||
/>
|
||||
|
||||
<.toggle_field
|
||||
field="pdp_reviews"
|
||||
label="Reviews section"
|
||||
checked={@theme_settings.pdp_reviews}
|
||||
event_prefix={@event_prefix}
|
||||
/>
|
||||
|
||||
<.toggle_field
|
||||
field="pdp_related_products"
|
||||
label="Related products"
|
||||
checked={@theme_settings.pdp_related_products}
|
||||
event_prefix={@event_prefix}
|
||||
/>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
# ── Helper Components ──────────────────────────────────────────────
|
||||
|
||||
attr :field, :string, required: true
|
||||
attr :label, :string, required: true
|
||||
attr :checked, :boolean, required: true
|
||||
attr :event_prefix, :string, default: ""
|
||||
|
||||
defp toggle_field(assigns) do
|
||||
~H"""
|
||||
<div class="theme-field">
|
||||
<label class="admin-check-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={@checked}
|
||||
phx-click={@event_prefix <> "toggle_setting"}
|
||||
phx-value-field={@field}
|
||||
class="admin-checkbox admin-checkbox-sm"
|
||||
/>
|
||||
<span class="theme-check-text">{@label}</span>
|
||||
</label>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user