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:
@@ -522,561 +522,39 @@
|
||||
<% end %>
|
||||
|
||||
<!-- Presets Section -->
|
||||
<div class="theme-section">
|
||||
<label class="theme-section-label">Start with a preset</label>
|
||||
<div class="theme-presets">
|
||||
<%= for {preset_name, description} <- @presets_with_descriptions do %>
|
||||
<button
|
||||
type="button"
|
||||
phx-click="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>
|
||||
<.preset_grid
|
||||
presets={@presets_with_descriptions}
|
||||
active_preset={@active_preset}
|
||||
event_prefix=""
|
||||
label="Start with a preset"
|
||||
/>
|
||||
|
||||
<!-- Accent Colors -->
|
||||
<div class="theme-section">
|
||||
<label class="theme-section-label">Accent colour</label>
|
||||
<form
|
||||
id="accent-color-form"
|
||||
phx-change="update_color"
|
||||
phx-value-field="accent_color"
|
||||
phx-hook="ColorSync"
|
||||
>
|
||||
<div class="theme-color-row">
|
||||
<input
|
||||
type="color"
|
||||
id="accent-color-picker"
|
||||
name="value"
|
||||
value={@theme_settings.accent_color}
|
||||
class="theme-color-swatch"
|
||||
/>
|
||||
<span class="theme-color-value">{@theme_settings.accent_color}</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="theme-section">
|
||||
<label class="theme-section-label">Hover colour</label>
|
||||
<form
|
||||
id="secondary-accent-color-form"
|
||||
phx-change="update_color"
|
||||
phx-value-field="secondary_accent_color"
|
||||
phx-hook="ColorSync"
|
||||
>
|
||||
<div class="theme-color-row">
|
||||
<input
|
||||
type="color"
|
||||
id="secondary-accent-color-picker"
|
||||
name="value"
|
||||
value={@theme_settings.secondary_accent_color}
|
||||
class="theme-color-swatch"
|
||||
/>
|
||||
<span class="theme-color-value">{@theme_settings.secondary_accent_color}</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="theme-section">
|
||||
<label class="theme-section-label">Sale colour</label>
|
||||
<form
|
||||
id="sale-color-form"
|
||||
phx-change="update_color"
|
||||
phx-value-field="sale_color"
|
||||
phx-hook="ColorSync"
|
||||
>
|
||||
<div class="theme-color-row">
|
||||
<input
|
||||
type="color"
|
||||
id="sale-color-picker"
|
||||
name="value"
|
||||
value={@theme_settings.sale_color}
|
||||
class="theme-color-swatch"
|
||||
/>
|
||||
<span class="theme-color-value">{@theme_settings.sale_color}</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<.color_picker
|
||||
field="accent_color"
|
||||
label="Accent colour"
|
||||
value={@theme_settings.accent_color}
|
||||
event_prefix=""
|
||||
/>
|
||||
<.color_picker
|
||||
field="secondary_accent_color"
|
||||
label="Hover colour"
|
||||
value={@theme_settings.secondary_accent_color}
|
||||
event_prefix=""
|
||||
/>
|
||||
<.color_picker
|
||||
field="sale_color"
|
||||
label="Sale colour"
|
||||
value={@theme_settings.sale_color}
|
||||
event_prefix=""
|
||||
/>
|
||||
|
||||
<!-- Customise Section -->
|
||||
<details
|
||||
class="theme-customise"
|
||||
id="customise-section"
|
||||
open={@customise_open}
|
||||
>
|
||||
<summary class="theme-customise-summary" phx-click="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 -->
|
||||
<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="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="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="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>
|
||||
|
||||
<!-- Colours Group -->
|
||||
<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="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>
|
||||
|
||||
<!-- Layout Group -->
|
||||
<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="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="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="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>
|
||||
|
||||
<div class="theme-field">
|
||||
<label class="admin-check-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={@theme_settings.announcement_bar}
|
||||
phx-click="toggle_setting"
|
||||
phx-value-field="announcement_bar"
|
||||
class="admin-checkbox admin-checkbox-sm"
|
||||
/>
|
||||
<span class="theme-check-text">Announcement bar</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="theme-field">
|
||||
<label class="admin-check-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={@theme_settings.sticky_header}
|
||||
phx-click="toggle_setting"
|
||||
phx-value-field="sticky_header"
|
||||
class="admin-checkbox admin-checkbox-sm"
|
||||
/>
|
||||
<span class="theme-check-text">Sticky header</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shape Group -->
|
||||
<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="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="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="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>
|
||||
|
||||
<!-- Products Group -->
|
||||
<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="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="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="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>
|
||||
|
||||
<div class="theme-field">
|
||||
<label class="admin-check-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={@theme_settings.hover_image}
|
||||
phx-click="toggle_setting"
|
||||
phx-value-field="hover_image"
|
||||
class="admin-checkbox admin-checkbox-sm"
|
||||
/>
|
||||
<span class="theme-check-text">Second image on hover</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="theme-field">
|
||||
<label class="admin-check-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={@theme_settings.show_prices}
|
||||
phx-click="toggle_setting"
|
||||
phx-value-field="show_prices"
|
||||
class="admin-checkbox admin-checkbox-sm"
|
||||
/>
|
||||
<span class="theme-check-text">Show prices</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Product Page Group -->
|
||||
<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>
|
||||
|
||||
<div class="theme-field">
|
||||
<label class="admin-check-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={@theme_settings.pdp_trust_badges}
|
||||
phx-click="toggle_setting"
|
||||
phx-value-field="pdp_trust_badges"
|
||||
class="admin-checkbox admin-checkbox-sm"
|
||||
/>
|
||||
<span class="theme-check-text">Trust badges</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="theme-field">
|
||||
<label class="admin-check-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={@theme_settings.pdp_reviews}
|
||||
phx-click="toggle_setting"
|
||||
phx-value-field="pdp_reviews"
|
||||
class="admin-checkbox admin-checkbox-sm"
|
||||
/>
|
||||
<span class="theme-check-text">Reviews section</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="theme-field">
|
||||
<label class="admin-check-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={@theme_settings.pdp_related_products}
|
||||
phx-click="toggle_setting"
|
||||
phx-value-field="pdp_related_products"
|
||||
class="admin-checkbox admin-checkbox-sm"
|
||||
/>
|
||||
<span class="theme-check-text">Related products</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<.customise_accordion
|
||||
theme_settings={@theme_settings}
|
||||
customise_open={@customise_open}
|
||||
event_prefix=""
|
||||
/>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user