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>
652 lines
22 KiB
Plaintext
652 lines
22 KiB
Plaintext
<div class="theme-layout">
|
||
<!-- Controls Sidebar -->
|
||
<div
|
||
id="theme-sidebar"
|
||
class={[
|
||
"theme-sidebar",
|
||
if(@sidebar_collapsed,
|
||
do: "theme-sidebar-collapsed",
|
||
else: "theme-sidebar-expanded"
|
||
)
|
||
]}
|
||
>
|
||
<!-- Collapsed state: just show expand button -->
|
||
<%= if @sidebar_collapsed do %>
|
||
<div class="theme-sidebar-collapsed-inner">
|
||
<button
|
||
type="button"
|
||
phx-click="toggle_sidebar"
|
||
class="theme-collapse-btn"
|
||
aria-label="Expand sidebar"
|
||
aria-expanded="false"
|
||
aria-controls="theme-sidebar"
|
||
>
|
||
<svg
|
||
class="theme-collapse-icon"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="2"
|
||
aria-hidden="true"
|
||
>
|
||
<polyline points="9 18 15 12 9 6"></polyline>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<% else %>
|
||
<.link href={~p"/admin"} class="theme-back-link">
|
||
<.icon name="hero-arrow-left-mini" class="size-4" /> Admin
|
||
</.link>
|
||
|
||
<div :if={@from_checklist} class="admin-checklist-banner">
|
||
<.icon name="hero-clipboard-document-check" class="size-5 admin-checklist-banner-icon" />
|
||
<span class="admin-checklist-banner-text">
|
||
You're customising your theme.
|
||
</span>
|
||
<.link navigate={~p"/admin"} class="admin-link admin-checklist-banner-link">
|
||
← Back to checklist
|
||
</.link>
|
||
</div>
|
||
|
||
<!-- Header -->
|
||
<div class="theme-header">
|
||
<div class="admin-fill">
|
||
<h1 class="theme-title">Theme</h1>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
phx-click="toggle_sidebar"
|
||
class="theme-collapse-btn"
|
||
aria-label="Collapse sidebar"
|
||
aria-expanded="true"
|
||
aria-controls="theme-sidebar"
|
||
>
|
||
<svg
|
||
class="theme-collapse-icon"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="2"
|
||
aria-hidden="true"
|
||
>
|
||
<polyline points="15 18 9 12 15 6"></polyline>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Site Name -->
|
||
<div class="theme-section">
|
||
<label class="theme-section-label">Shop name</label>
|
||
<form phx-change="update_setting" phx-value-field="site_name">
|
||
<input
|
||
type="text"
|
||
name="site_name"
|
||
value={@site_name}
|
||
placeholder="Your shop name"
|
||
class="admin-input admin-input-lg"
|
||
/>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Branding Section -->
|
||
<div class="theme-panel">
|
||
<span class="theme-section-label">Logo & header</span>
|
||
|
||
<div class="admin-stack admin-stack-sm theme-field">
|
||
<label class="admin-toggle-label">
|
||
<input
|
||
type="checkbox"
|
||
checked={@theme_settings.show_site_name}
|
||
phx-click="toggle_setting"
|
||
phx-value-field="show_site_name"
|
||
class="admin-toggle admin-toggle-sm"
|
||
/>
|
||
<span class="theme-slider-label">Show shop name</span>
|
||
</label>
|
||
|
||
<label class="admin-toggle-label">
|
||
<input
|
||
type="checkbox"
|
||
checked={@theme_settings.show_logo}
|
||
phx-click="toggle_setting"
|
||
phx-value-field="show_logo"
|
||
class="admin-toggle admin-toggle-sm"
|
||
/>
|
||
<span class="theme-slider-label">Show logo</span>
|
||
</label>
|
||
</div>
|
||
|
||
<!-- Logo Upload (when logo enabled) -->
|
||
<%= if @theme_settings.show_logo do %>
|
||
<div class="theme-subsection">
|
||
<span class="theme-slider-label theme-block-label">
|
||
Upload logo (SVG or PNG)
|
||
</span>
|
||
<div class="admin-row admin-row-lg">
|
||
<form phx-change="noop" phx-submit="noop" class="admin-fill">
|
||
<label class="theme-upload-label">
|
||
<span>Choose file...</span>
|
||
<.live_file_input upload={@uploads.logo_upload} class="hidden" />
|
||
</label>
|
||
</form>
|
||
<%= if @logo_image do %>
|
||
<div class="theme-thumb theme-thumb-logo">
|
||
<img
|
||
src={"/image_cache/#{@logo_image.id}.webp"}
|
||
alt={@site_name}
|
||
/>
|
||
<button
|
||
type="button"
|
||
phx-click="remove_logo"
|
||
class="theme-remove-btn"
|
||
title="Remove logo"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
<% end %>
|
||
</div>
|
||
|
||
<%= for entry <- @uploads.logo_upload.entries do %>
|
||
<div class="theme-progress">
|
||
<div class="theme-progress-bar">
|
||
<div
|
||
class="theme-progress-fill"
|
||
style={"width: #{entry.progress}%"}
|
||
>
|
||
</div>
|
||
</div>
|
||
<span class="admin-text-secondary">{entry.progress}%</span>
|
||
<button
|
||
type="button"
|
||
phx-click="cancel_upload"
|
||
phx-value-ref={entry.ref}
|
||
phx-value-upload="logo_upload"
|
||
class="theme-upload-cancel"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
<%= for err <- upload_errors(@uploads.logo_upload, entry) do %>
|
||
<p class="theme-error-text">{error_to_string(err)}</p>
|
||
<% end %>
|
||
<% end %>
|
||
|
||
<%= for err <- upload_errors(@uploads.logo_upload) do %>
|
||
<p class="theme-error-text">{error_to_string(err)}</p>
|
||
<% end %>
|
||
|
||
<!-- Logo Size Slider -->
|
||
<%= if @logo_image do %>
|
||
<form
|
||
phx-change="update_setting"
|
||
phx-value-field="logo_size"
|
||
class="theme-subfield"
|
||
>
|
||
<div class="theme-slider-header">
|
||
<span class="theme-slider-label">Logo size</span>
|
||
<span class="theme-slider-value">{@theme_settings.logo_size}px</span>
|
||
</div>
|
||
<input
|
||
type="range"
|
||
min="24"
|
||
max="120"
|
||
value={@theme_settings.logo_size}
|
||
name="logo_size"
|
||
class="admin-range"
|
||
/>
|
||
</form>
|
||
|
||
<!-- SVG Recolor Toggle (only for SVG logos) -->
|
||
<%= if @logo_image.is_svg do %>
|
||
<div class="theme-subfield">
|
||
<label class="admin-toggle-label">
|
||
<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="admin-toggle admin-toggle-sm"
|
||
/>
|
||
<span class="theme-slider-label">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="theme-color-row theme-subfield-sm"
|
||
>
|
||
<input
|
||
type="color"
|
||
name="value"
|
||
value={@theme_settings.logo_color}
|
||
class="theme-color-swatch theme-color-swatch-sm"
|
||
/>
|
||
<span class="theme-color-value">{@theme_settings.logo_color}</span>
|
||
</form>
|
||
<% end %>
|
||
</div>
|
||
<% end %>
|
||
<% end %>
|
||
</div>
|
||
<% end %>
|
||
</div>
|
||
|
||
<!-- Site Icon / Favicon -->
|
||
<div class="theme-panel">
|
||
<label class="theme-section-label">Site icon</label>
|
||
<p class="admin-text-tertiary theme-field">
|
||
Your icon appears in browser tabs and on home screens.
|
||
</p>
|
||
|
||
<!-- Use logo as icon toggle -->
|
||
<label class="admin-toggle-label theme-field">
|
||
<input
|
||
type="checkbox"
|
||
checked={@theme_settings.use_logo_as_icon}
|
||
phx-click="toggle_setting"
|
||
phx-value-field="use_logo_as_icon"
|
||
class="admin-toggle admin-toggle-sm"
|
||
/>
|
||
<span class="theme-slider-label">Use logo as favicon</span>
|
||
</label>
|
||
|
||
<!-- Icon upload (only when not using logo) -->
|
||
<%= if !@theme_settings.use_logo_as_icon do %>
|
||
<div class="admin-separator">
|
||
<span class="theme-slider-label theme-block-label">
|
||
Upload icon (PNG or SVG, 512×512+)
|
||
</span>
|
||
<div class="admin-row admin-row-lg">
|
||
<form phx-change="noop" phx-submit="noop" class="admin-fill">
|
||
<label class="theme-upload-label">
|
||
<span>Choose file...</span>
|
||
<.live_file_input upload={@uploads.icon_upload} class="hidden" />
|
||
</label>
|
||
</form>
|
||
<%= if @icon_image do %>
|
||
<div class="theme-thumb theme-thumb-icon">
|
||
<%= if @icon_image.is_svg do %>
|
||
<img
|
||
src={"/images/#{@icon_image.id}/recolored/000000"}
|
||
alt="Current icon"
|
||
/>
|
||
<% else %>
|
||
<img
|
||
src={"/image_cache/#{@icon_image.id}.webp"}
|
||
alt="Current icon"
|
||
/>
|
||
<% end %>
|
||
<button
|
||
type="button"
|
||
phx-click="remove_icon"
|
||
class="theme-remove-btn"
|
||
title="Remove icon"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
<% end %>
|
||
</div>
|
||
|
||
<%= for entry <- @uploads.icon_upload.entries do %>
|
||
<div class="theme-progress">
|
||
<div class="theme-progress-bar">
|
||
<div
|
||
class="theme-progress-fill"
|
||
style={"width: #{entry.progress}%"}
|
||
>
|
||
</div>
|
||
</div>
|
||
<span class="admin-text-secondary">{entry.progress}%</span>
|
||
<button
|
||
type="button"
|
||
phx-click="cancel_upload"
|
||
phx-value-ref={entry.ref}
|
||
phx-value-upload="icon_upload"
|
||
class="theme-upload-cancel"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
<%= for err <- upload_errors(@uploads.icon_upload, entry) do %>
|
||
<p class="theme-error-text">{error_to_string(err)}</p>
|
||
<% end %>
|
||
<% end %>
|
||
|
||
<%= for err <- upload_errors(@uploads.icon_upload) do %>
|
||
<p class="theme-error-text">{error_to_string(err)}</p>
|
||
<% end %>
|
||
</div>
|
||
<% end %>
|
||
|
||
<!-- Short name -->
|
||
<div class="theme-subfield">
|
||
<form phx-change="update_setting" phx-value-field="favicon_short_name">
|
||
<label class="theme-slider-label theme-block-label">
|
||
Short name <span class="admin-text-tertiary">— appears under home screen icon</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
name="favicon_short_name"
|
||
value={@theme_settings.favicon_short_name}
|
||
placeholder={String.slice(@site_name, 0, 12)}
|
||
maxlength="12"
|
||
class="admin-input admin-input-sm"
|
||
/>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Icon background colour -->
|
||
<div class="theme-subfield">
|
||
<form
|
||
id="icon-bg-color-form"
|
||
phx-change="update_color"
|
||
phx-value-field="icon_background_color"
|
||
phx-hook="ColorSync"
|
||
class="theme-color-row"
|
||
>
|
||
<input
|
||
type="color"
|
||
name="value"
|
||
value={@theme_settings.icon_background_color}
|
||
class="theme-color-swatch theme-color-swatch-sm"
|
||
/>
|
||
<div>
|
||
<span class="theme-slider-label theme-block-label">Icon background</span>
|
||
<span class="theme-slider-value">
|
||
{@theme_settings.icon_background_color}
|
||
</span>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Header Background Toggle -->
|
||
<div class="theme-section">
|
||
<label class="admin-toggle-label">
|
||
<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="admin-toggle admin-toggle-sm"
|
||
/>
|
||
<span class="theme-check-text">
|
||
Header background image
|
||
</span>
|
||
</label>
|
||
</div>
|
||
|
||
<!-- Header Image Upload (only when enabled) -->
|
||
<%= if @theme_settings.header_background_enabled do %>
|
||
<div class="theme-panel">
|
||
<span class="theme-slider-label theme-block-label">
|
||
Upload header image
|
||
</span>
|
||
<form phx-change="noop" phx-submit="noop">
|
||
<label class="theme-upload-label">
|
||
<span>Choose file...</span>
|
||
<.live_file_input upload={@uploads.header_upload} class="hidden" />
|
||
</label>
|
||
</form>
|
||
|
||
<%= if @header_image do %>
|
||
<div class="theme-thumb theme-thumb-cover theme-thumb-header">
|
||
<img
|
||
src={"/image_cache/#{@header_image.id}.webp"}
|
||
alt=""
|
||
/>
|
||
<button
|
||
type="button"
|
||
phx-click="remove_header"
|
||
class="theme-remove-btn"
|
||
title="Remove header background"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
|
||
<%= if @header_contrast_warning != :ok do %>
|
||
<div class="theme-contrast-warning">
|
||
<.icon name="hero-exclamation-triangle" class="size-5" />
|
||
<div>
|
||
<strong>
|
||
<%= if @header_contrast_warning == :poor do %>
|
||
Text may be hard to read
|
||
<% else %>
|
||
Text contrast could be better
|
||
<% end %>
|
||
</strong>
|
||
<p>
|
||
The header text might blend into this background.
|
||
Try switching to a
|
||
<%= if @theme_settings.mood == "dark" do %>
|
||
lighter mood
|
||
<% else %>
|
||
dark mood
|
||
<% end %>
|
||
or choosing a different image.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<% end %>
|
||
|
||
<!-- Header Image Controls -->
|
||
<div class="admin-stack admin-stack-md theme-subfield">
|
||
<form phx-change="update_setting" phx-value-field="header_zoom">
|
||
<div class="theme-slider-header">
|
||
<span class="theme-slider-label">Zoom</span>
|
||
<span class="theme-slider-value">{@theme_settings.header_zoom}%</span>
|
||
</div>
|
||
<input
|
||
type="range"
|
||
min="100"
|
||
max="200"
|
||
value={@theme_settings.header_zoom}
|
||
name="header_zoom"
|
||
class="admin-range"
|
||
/>
|
||
</form>
|
||
<%= if @theme_settings.header_zoom > 100 do %>
|
||
<form phx-change="update_setting" phx-value-field="header_position_x">
|
||
<div class="theme-slider-header">
|
||
<span class="theme-slider-label">Horizontal position</span>
|
||
<span class="theme-slider-value">{@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="admin-range"
|
||
/>
|
||
</form>
|
||
<form phx-change="update_setting" phx-value-field="header_position_y">
|
||
<div class="theme-slider-header">
|
||
<span class="theme-slider-label">Vertical position</span>
|
||
<span class="theme-slider-value">{@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="admin-range"
|
||
/>
|
||
</form>
|
||
<% end %>
|
||
</div>
|
||
<% end %>
|
||
|
||
<%= for entry <- @uploads.header_upload.entries do %>
|
||
<div class="theme-progress">
|
||
<div class="theme-progress-bar">
|
||
<div
|
||
class="theme-progress-fill"
|
||
style={"width: #{entry.progress}%"}
|
||
>
|
||
</div>
|
||
</div>
|
||
<span class="admin-text-secondary">{entry.progress}%</span>
|
||
<button
|
||
type="button"
|
||
phx-click="cancel_upload"
|
||
phx-value-ref={entry.ref}
|
||
phx-value-upload="header_upload"
|
||
class="theme-upload-cancel"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
<%= for err <- upload_errors(@uploads.header_upload, entry) do %>
|
||
<p class="theme-error-text">{error_to_string(err)}</p>
|
||
<% end %>
|
||
<% end %>
|
||
|
||
<%= for err <- upload_errors(@uploads.header_upload) do %>
|
||
<p class="theme-error-text">{error_to_string(err)}</p>
|
||
<% end %>
|
||
</div>
|
||
<% end %>
|
||
|
||
<!-- Presets Section -->
|
||
<.preset_grid
|
||
presets={@presets_with_descriptions}
|
||
active_preset={@active_preset}
|
||
event_prefix=""
|
||
label="Start with a preset"
|
||
/>
|
||
|
||
<!-- Accent Colors -->
|
||
<.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 -->
|
||
<.customise_accordion
|
||
theme_settings={@theme_settings}
|
||
customise_open={@customise_open}
|
||
event_prefix=""
|
||
/>
|
||
<% end %>
|
||
</div>
|
||
|
||
<!-- Preview Area -->
|
||
<div class="theme-preview-area">
|
||
<div class="theme-preview-container">
|
||
<!-- Preview Page Switcher -->
|
||
<div class="theme-preview-tabs">
|
||
<%= for {page_name, label} <- [
|
||
{:home, "Home"},
|
||
{:collection, "Collection"},
|
||
{:pdp, "Product"},
|
||
{:cart, "Cart"},
|
||
{:about, "About"},
|
||
{:delivery, "Delivery"},
|
||
{:privacy, "Privacy"},
|
||
{:terms, "Terms"},
|
||
{:contact, "Contact"},
|
||
{:error, "404"}
|
||
] do %>
|
||
<button
|
||
type="button"
|
||
phx-click="change_preview_page"
|
||
phx-value-page={page_name}
|
||
class={[
|
||
"theme-preview-tab",
|
||
@preview_page == page_name && "theme-preview-tab-active"
|
||
]}
|
||
>
|
||
{label}
|
||
</button>
|
||
<% end %>
|
||
</div>
|
||
|
||
<!-- Browser Chrome -->
|
||
<div class="theme-browser-chrome">
|
||
<div class="theme-browser-dots">
|
||
<div class="theme-browser-dot theme-browser-dot-close"></div>
|
||
<div class="theme-browser-dot theme-browser-dot-min"></div>
|
||
<div class="theme-browser-dot theme-browser-dot-max"></div>
|
||
</div>
|
||
<div class="theme-browser-url">
|
||
<svg
|
||
class="theme-browser-url-icon"
|
||
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="theme-browser-url-text truncate">
|
||
{@site_name |> String.downcase() |> String.replace(" ", "")}.myshopify.com
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Preview Frame -->
|
||
<div
|
||
class="themed theme-preview-frame"
|
||
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}
|
||
data-sticky={to_string(@theme_settings.sticky_header)}
|
||
data-layout={@theme_settings.layout_width}
|
||
data-shadow={@theme_settings.card_shadow}
|
||
data-button-style={@theme_settings.button_style}
|
||
>
|
||
<style>
|
||
/* All font faces for theme switching */
|
||
<%= Phoenix.HTML.raw(Berrypod.Theme.Fonts.generate_all_font_faces(
|
||
&BerrypodWeb.Endpoint.static_path/1
|
||
)) %>
|
||
/* Generated theme CSS */
|
||
<%= Phoenix.HTML.raw(@generated_css) %>
|
||
</style>
|
||
|
||
<.preview_page
|
||
page={@preview_page}
|
||
preview_data={@preview_data}
|
||
theme_settings={@theme_settings}
|
||
site_name={@site_name}
|
||
logo_image={@logo_image}
|
||
header_image={@header_image}
|
||
cart_drawer_open={@cart_drawer_open}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|