consolidate shop pages into unified LiveView for editor state persistence
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:
jamey
2026-03-09 14:47:50 +00:00
parent ae0a149ecd
commit bb5d220079
29 changed files with 1410 additions and 1037 deletions

View File

@@ -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>