berrypod/lib/berrypod_web/live/admin/theme/index.html.heex
jamey bb5d220079
All checks were successful
deploy / deploy (push) Successful in 1m27s
consolidate shop pages into unified LiveView for editor state persistence
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>
2026-03-09 14:47:50 +00:00

652 lines
22 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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">
&larr; 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>