move branding settings from Theme tab to Site tab
Some checks failed
deploy / deploy (push) Failing after 10m13s

- Add branding_editor component to site_editor.ex with:
  - Shop name input
  - Show shop name / Show logo toggles
  - Logo upload (size slider, SVG recolor, color picker)
  - Header background toggle and upload (zoom, position sliders)
- Add site_ prefixed event handlers in page_editor_hook.ex:
  - site_update_branding, site_toggle_branding, site_update_color
  - site_remove_logo and site_remove_header (delegate to theme handlers)
- Remove branding sections from theme_editor.ex:
  - Deleted shop_name_input, branding_section, logo/header upload sections
- Theme tab now shows only: preset grid, accent colours, customise accordion

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jamey 2026-03-28 23:40:48 +00:00
parent 7c07805df8
commit 242fed0501
6 changed files with 458 additions and 376 deletions

View File

@ -160,13 +160,13 @@ Restructure the 3-tab editor panel for better discoverability. Replace Settings
| 8-9 | Social links editor (CRUD, reorder, platform detection, URL normalization) | 2h | done | | 8-9 | Social links editor (CRUD, reorder, platform detection, URL normalization) | 2h | done |
| 10-14 | Header & footer navigation editors | 3h | done | | 10-14 | Header & footer navigation editors | 3h | done |
| 15-16 | Footer content (about, copyright, newsletter toggle) | 1.25h | done | | 15-16 | Footer content (about, copyright, newsletter toggle) | 1.25h | done |
| 17-18 | Move branding from Theme to Site | 1.5h | planned | | 17-18 | Move branding from Theme to Site | 1.5h | done |
| 19-20 | Merge page settings into Page tab, remove Settings tab | 1h | planned | | 19-20 | Merge page settings into Page tab, remove Settings tab | 1h | planned |
| 21-22 | Polish and testing | 2h | planned | | 21-22 | Polish and testing | 2h | planned |
Social links now support 40+ platforms (grouped by category), auto-detect platform from pasted URLs (including deep links like `tg://` and `spotify:`), normalize bare domains to https://, preserve custom protocols, and filter empty URLs from shop display. Social links now support 40+ platforms (grouped by category), auto-detect platform from pasted URLs (including deep links like `tg://` and `spotify:`), normalize bare domains to https://, preserve custom protocols, and filter empty URLs from shop display.
Navigation editors support add/edit/delete/reorder for both header and footer nav items. Page picker dropdown allows linking to system pages (home, about, contact, etc.) or custom published pages. Items persist immediately on change. Navigation editors support add/edit/delete/reorder for both header and footer nav items. Page picker dropdown allows linking to system pages (home, about, contact, etc.) or custom published pages. Changes preview immediately with live update in the shop layout. Removed legacy /admin/navigation page — nav editing is now exclusively through the Site tab with live preview.
### Unified editor session ([plan](docs/plans/unified-editor-session.md)) — Complete ### Unified editor session ([plan](docs/plans/unified-editor-session.md)) — Complete

View File

@ -213,8 +213,8 @@ end
| 14 | Footer nav: read from database | 13 | 30m | done | | 14 | Footer nav: read from database | 13 | 30m | done |
| 15 | Footer content: about, copyright, newsletter toggle | 4 | 45m | done | | 15 | Footer content: about, copyright, newsletter toggle | 4 | 45m | done |
| 16 | Footer: read new fields | 15 | 30m | done | | 16 | Footer: read new fields | 15 | 30m | done |
| 17 | Move branding settings from Theme to Site | 4 | 1h | planned | | 17 | Move branding settings from Theme to Site | 4 | 1h | done |
| 18 | Theme tab: remove branding, polish remaining | 17 | 30m | planned | | 18 | Theme tab: remove branding, polish remaining | 17 | 30m | done |
| 19 | Merge page settings into Page tab | — | 45m | planned | | 19 | Merge page settings into Page tab | — | 45m | planned |
| 20 | Remove Settings tab | 19 | 15m | planned | | 20 | Remove Settings tab | 19 | 15m | planned |
| 21 | Polish: responsive, empty states, validation | 1-20 | 1.5h | planned | | 21 | Polish: responsive, empty states, validation | 1-20 | 1.5h | planned |
@ -300,6 +300,34 @@ Header and footer navigation items are managed through a unified `nav_editor` co
- `lib/berrypod_web/page_editor_hook.ex` - Event handlers, sync functions, dirty state - `lib/berrypod_web/page_editor_hook.ex` - Event handlers, sync functions, dirty state
- `assets/css/admin/components.css` - Styles for nav editor - `assets/css/admin/components.css` - Styles for nav editor
### Completed: Branding Migration (Tasks 17-18)
Moved branding settings from Theme tab to Site tab.
**What moved:**
- Shop name input
- Show shop name / Show logo toggles
- Logo upload (file picker, size slider, recolor toggle, color picker)
- Header background toggle
- Header image upload (file picker, zoom slider, position sliders)
**Event routing:**
- Site tab branding events use `site_` prefix (e.g., `site_update_branding`, `site_toggle_branding`)
- These route to the same underlying theme state (`theme_editor_settings`, `theme_editor_logo_image`, etc.)
- Logo/header uploads still use `theme_logo_upload` and `theme_header_upload` (shared with theme save flow)
- `site_remove_logo` and `site_remove_header` delegate to theme action handlers
**Files modified:**
- `lib/berrypod_web/page_renderer.ex` - Pass branding props to site_editor
- `lib/berrypod_web/components/shop_components/site_editor.ex` - Add `branding_editor` component with logo/header upload sections
- `lib/berrypod_web/page_editor_hook.ex` - Add `site_update_branding`, `site_toggle_branding`, `site_update_color`, `site_remove_logo`, `site_remove_header` handlers
- `lib/berrypod_web/components/shop_components/theme_editor.ex` - Remove `shop_name_input`, `branding_section`, logo/header upload sections
**Theme tab now contains:**
- Preset grid (8 themes)
- Accent, hover, sale colours
- Customise accordion with typography, colours, layout, shape, products, product page groups
## UI Wireframes ## UI Wireframes
### Site tab layout ### Site tab layout

View File

@ -24,10 +24,18 @@ defmodule BerrypodWeb.ShopComponents.SiteEditor do
Expects: Expects:
- site_state: SiteEditorState struct with all site tab state - site_state: SiteEditorState struct with all site tab state
- site_nav_pages: list of pages for the nav picker - site_nav_pages: list of pages for the nav picker
- Branding-related props passed from theme state
""" """
attr :site_state, :any, required: true attr :site_state, :any, required: true
attr :site_nav_pages, :list, default: [] attr :site_nav_pages, :list, default: []
attr :event_prefix, :string, default: "site_" attr :event_prefix, :string, default: "site_"
attr :site_name, :string, default: ""
attr :theme_settings, :map, default: nil
attr :logo_image, :map, default: nil
attr :header_image, :map, default: nil
attr :icon_image, :map, default: nil
attr :contrast_warning, :atom, default: :ok
attr :uploads, :map, default: nil
def site_editor(%{site_state: nil} = assigns) do def site_editor(%{site_state: nil} = assigns) do
~H""" ~H"""
@ -60,7 +68,16 @@ defmodule BerrypodWeb.ShopComponents.SiteEditor do
~H""" ~H"""
<div class="editor-site-content"> <div class="editor-site-content">
<.site_section title="Branding" icon="hero-sparkles" open={true}> <.site_section title="Branding" icon="hero-sparkles" open={true}>
<.branding_placeholder /> <.branding_editor
site_name={@site_name}
theme_settings={@theme_settings}
logo_image={@logo_image}
header_image={@header_image}
icon_image={@icon_image}
contrast_warning={@contrast_warning}
uploads={@uploads}
event_prefix={@event_prefix}
/>
</.site_section> </.site_section>
<.site_section title="Announcement bar" icon="hero-megaphone"> <.site_section title="Announcement bar" icon="hero-megaphone">
@ -128,19 +145,356 @@ defmodule BerrypodWeb.ShopComponents.SiteEditor do
""" """
end end
# ── Branding Section (placeholder) ───────────────────────────────── # ── Branding Editor ─────────────────────────────────────────────────
defp branding_placeholder(assigns) do attr :site_name, :string, default: ""
attr :theme_settings, :map, default: nil
attr :logo_image, :map, default: nil
attr :header_image, :map, default: nil
attr :icon_image, :map, default: nil
attr :contrast_warning, :atom, default: :ok
attr :uploads, :map, default: nil
attr :event_prefix, :string, default: "site_"
defp branding_editor(%{theme_settings: nil} = assigns) do
~H""" ~H"""
<div class="site-editor-placeholder"> <p class="admin-text-secondary">Loading branding settings...</p>
<p class="admin-text-secondary"> """
Branding settings (shop name, logo, favicon) will be moved here from the Theme tab. end
</p>
<p class="admin-help-text">Coming soon in the next phase.</p> defp branding_editor(assigns) do
~H"""
<div class="site-editor-form">
<%!-- Shop name --%>
<div class="theme-section">
<label class="theme-section-label">Shop name</label>
<form phx-change={@event_prefix <> "update_branding"} phx-value-field="site_name">
<input
type="text"
name="site_name"
value={@site_name}
placeholder="Your shop name"
class="admin-input"
/>
</form>
</div>
<%!-- Display toggles --%>
<div class="admin-stack admin-stack-sm theme-field">
<label class="admin-check-label">
<input
type="checkbox"
checked={@theme_settings.show_site_name}
phx-click={@event_prefix <> "toggle_branding"}
phx-value-field="show_site_name"
class="admin-checkbox admin-checkbox-sm"
/>
<span class="theme-check-text">Show shop name</span>
</label>
<label class="admin-check-label">
<input
type="checkbox"
checked={@theme_settings.show_logo}
phx-click={@event_prefix <> "toggle_branding"}
phx-value-field="show_logo"
class="admin-checkbox admin-checkbox-sm"
/>
<span class="theme-check-text">Show logo</span>
</label>
</div>
<%!-- Logo upload section --%>
<%= if @theme_settings.show_logo && @uploads do %>
<.logo_upload_section
uploads={@uploads}
logo_image={@logo_image}
theme_settings={@theme_settings}
site_name={@site_name}
event_prefix={@event_prefix}
/>
<% end %>
<%!-- Header background toggle --%>
<label class="admin-check-label theme-field">
<input
type="checkbox"
checked={@theme_settings.header_background_enabled}
phx-click={@event_prefix <> "update_branding"}
phx-value-field="header_background_enabled"
phx-value-setting_value={
if @theme_settings.header_background_enabled, do: "false", else: "true"
}
class="admin-checkbox admin-checkbox-sm"
/>
<span class="theme-check-text">Header background image</span>
</label>
<%!-- Header upload section --%>
<%= if @theme_settings.header_background_enabled && @uploads do %>
<.header_upload_section
uploads={@uploads}
header_image={@header_image}
theme_settings={@theme_settings}
contrast_warning={@contrast_warning}
event_prefix={@event_prefix}
/>
<% end %>
</div> </div>
""" """
end end
# Logo upload sub-section
attr :uploads, :map, required: true
attr :logo_image, :map, default: nil
attr :theme_settings, :map, required: true
attr :site_name, :string, default: ""
attr :event_prefix, :string, default: "site_"
defp logo_upload_section(assigns) do
~H"""
<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 theme-upload-label-compact">
<span>Choose file...</span>
<.live_file_input upload={@uploads.theme_logo_upload} class="hidden" />
</label>
</form>
<%= if @logo_image do %>
<div class="theme-thumb theme-thumb-logo theme-thumb-compact">
<img src={"/image_cache/#{@logo_image.id}.webp"} alt={@site_name} />
<button
type="button"
phx-click={@event_prefix <> "remove_logo"}
class="theme-remove-btn"
title="Remove logo"
>
×
</button>
</div>
<% end %>
</div>
<%= for entry <- @uploads.theme_logo_upload.entries do %>
<.upload_progress entry={entry} upload_name="theme_logo_upload" />
<%= for err <- upload_errors(@uploads.theme_logo_upload, entry) do %>
<p class="theme-error-text">{error_to_string(err)}</p>
<% end %>
<% end %>
<%= for err <- upload_errors(@uploads.theme_logo_upload) do %>
<p class="theme-error-text">{error_to_string(err)}</p>
<% end %>
<%= if @logo_image do %>
<form
phx-change={@event_prefix <> "update_branding"}
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>
<%= if @logo_image.is_svg do %>
<div class="theme-subfield">
<label class="admin-check-label">
<input
type="checkbox"
checked={@theme_settings.logo_recolor}
phx-click={@event_prefix <> "update_branding"}
phx-value-field="logo_recolor"
phx-value-setting_value={if @theme_settings.logo_recolor, do: "false", else: "true"}
class="admin-checkbox admin-checkbox-sm"
/>
<span class="theme-check-text">Recolour logo</span>
</label>
<%= if @theme_settings.logo_recolor do %>
<form
id="logo-color-form-site"
phx-change={@event_prefix <> "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
# Header upload sub-section
attr :uploads, :map, required: true
attr :header_image, :map, default: nil
attr :theme_settings, :map, required: true
attr :contrast_warning, :atom, default: :ok
attr :event_prefix, :string, default: "site_"
defp header_upload_section(assigns) do
~H"""
<div class="theme-subsection">
<span class="theme-slider-label theme-block-label">Upload header image</span>
<form phx-change="noop" phx-submit="noop">
<label class="theme-upload-label theme-upload-label-compact">
<span>Choose file...</span>
<.live_file_input upload={@uploads.theme_header_upload} class="hidden" />
</label>
</form>
<%= if @header_image do %>
<div class="theme-thumb theme-thumb-cover theme-thumb-header theme-thumb-compact">
<img src={"/image_cache/#{@header_image.id}.webp"} alt="" />
<button
type="button"
phx-click={@event_prefix <> "remove_header"}
class="theme-remove-btn"
title="Remove header background"
>
×
</button>
</div>
<%= if @contrast_warning != :ok do %>
<div class="theme-contrast-warning theme-contrast-warning-compact">
<strong>
<%= if @contrast_warning == :poor do %>
Text may be hard to read
<% else %>
Text contrast could be better
<% end %>
</strong>
<p>
Try switching to a
<%= if @theme_settings.mood == "dark" do %>
lighter mood
<% else %>
dark mood
<% end %>
or choosing a different image.
</p>
</div>
<% end %>
<div class="admin-stack admin-stack-md theme-subfield">
<form phx-change={@event_prefix <> "update_branding"} 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={@event_prefix <> "update_branding"}
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={@event_prefix <> "update_branding"}
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.theme_header_upload.entries do %>
<.upload_progress entry={entry} upload_name="theme_header_upload" />
<%= for err <- upload_errors(@uploads.theme_header_upload, entry) do %>
<p class="theme-error-text">{error_to_string(err)}</p>
<% end %>
<% end %>
<%= for err <- upload_errors(@uploads.theme_header_upload) do %>
<p class="theme-error-text">{error_to_string(err)}</p>
<% end %>
</div>
"""
end
# Shared upload progress component
attr :entry, :map, required: true
attr :upload_name, :string, required: true
defp upload_progress(assigns) do
~H"""
<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="theme_cancel_upload"
phx-value-ref={@entry.ref}
phx-value-upload={@upload_name}
class="theme-upload-cancel"
>
×
</button>
</div>
"""
end
defp error_to_string(:too_large), do: "File is too large"
defp error_to_string(:too_many_files), do: "Too many files"
defp error_to_string(:not_accepted), do: "File type not accepted"
defp error_to_string(err), do: inspect(err)
# ── Announcement Bar Editor ───────────────────────────────────────── # ── Announcement Bar Editor ─────────────────────────────────────────
attr :settings, :map, required: true attr :settings, :map, required: true

View File

@ -18,29 +18,6 @@ defmodule BerrypodWeb.ShopComponents.ThemeEditor do
# ── Quick Settings ───────────────────────────────────────────────── # ── Quick Settings ─────────────────────────────────────────────────
# These are the core settings shown in both compact and full modes. # 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 """ @doc """
Renders the preset grid for quick theme switching. Renders the preset grid for quick theme switching.
""" """
@ -211,19 +188,6 @@ defmodule BerrypodWeb.ShopComponents.ThemeEditor do
~H""" ~H"""
<div class="editor-theme-content"> <div class="editor-theme-content">
<%= if @theme_settings do %> <%= if @theme_settings do %>
<.shop_name_input site_name={@site_name} event_prefix={@event_prefix} />
<.branding_section
theme_settings={@theme_settings}
uploads={@uploads}
logo_image={@logo_image}
header_image={@header_image}
icon_image={@icon_image}
contrast_warning={@contrast_warning}
site_name={@site_name}
event_prefix={@event_prefix}
/>
<.preset_grid presets={@presets} active_preset={@active_preset} event_prefix={@event_prefix} /> <.preset_grid presets={@presets} active_preset={@active_preset} event_prefix={@event_prefix} />
<.color_picker <.color_picker
@ -257,334 +221,6 @@ defmodule BerrypodWeb.ShopComponents.ThemeEditor do
""" """
end end
# ── Branding Section (Logo, Header, Icon) ───────────────────────────
attr :theme_settings, :map, required: true
attr :uploads, :map, default: nil
attr :logo_image, :map, default: nil
attr :header_image, :map, default: nil
attr :icon_image, :map, default: nil
attr :contrast_warning, :atom, default: :ok
attr :site_name, :string, default: ""
attr :event_prefix, :string, default: "theme_"
defp branding_section(assigns) do
~H"""
<div class="theme-section theme-section-compact">
<label class="theme-section-label">Logo & branding</label>
<div class="admin-stack admin-stack-sm theme-field">
<label class="admin-check-label">
<input
type="checkbox"
checked={@theme_settings.show_site_name}
phx-click={@event_prefix <> "toggle_setting"}
phx-value-field="show_site_name"
class="admin-checkbox admin-checkbox-sm"
/>
<span class="theme-check-text">Show shop name</span>
</label>
<label class="admin-check-label">
<input
type="checkbox"
checked={@theme_settings.show_logo}
phx-click={@event_prefix <> "toggle_setting"}
phx-value-field="show_logo"
class="admin-checkbox admin-checkbox-sm"
/>
<span class="theme-check-text">Show logo</span>
</label>
</div>
<%= if @theme_settings.show_logo && @uploads do %>
<.logo_upload_section
uploads={@uploads}
logo_image={@logo_image}
theme_settings={@theme_settings}
site_name={@site_name}
event_prefix={@event_prefix}
/>
<% end %>
<label class="admin-check-label theme-field">
<input
type="checkbox"
checked={@theme_settings.header_background_enabled}
phx-click={@event_prefix <> "update_setting"}
phx-value-field="header_background_enabled"
phx-value-setting_value={
if @theme_settings.header_background_enabled, do: "false", else: "true"
}
class="admin-checkbox admin-checkbox-sm"
/>
<span class="theme-check-text">Header background image</span>
</label>
<%= if @theme_settings.header_background_enabled && @uploads do %>
<.header_upload_section
uploads={@uploads}
header_image={@header_image}
theme_settings={@theme_settings}
contrast_warning={@contrast_warning}
event_prefix={@event_prefix}
/>
<% end %>
</div>
"""
end
# Logo upload sub-section
attr :uploads, :map, required: true
attr :logo_image, :map, default: nil
attr :theme_settings, :map, required: true
attr :site_name, :string, default: ""
attr :event_prefix, :string, default: "theme_"
defp logo_upload_section(assigns) do
~H"""
<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 theme-upload-label-compact">
<span>Choose file...</span>
<.live_file_input upload={@uploads.theme_logo_upload} class="hidden" />
</label>
</form>
<%= if @logo_image do %>
<div class="theme-thumb theme-thumb-logo theme-thumb-compact">
<img src={"/image_cache/#{@logo_image.id}.webp"} alt={@site_name} />
<button
type="button"
phx-click={@event_prefix <> "remove_logo"}
class="theme-remove-btn"
title="Remove logo"
>
×
</button>
</div>
<% end %>
</div>
<%= for entry <- @uploads.theme_logo_upload.entries do %>
<.upload_progress entry={entry} upload_name="theme_logo_upload" />
<%= for err <- upload_errors(@uploads.theme_logo_upload, entry) do %>
<p class="theme-error-text">{error_to_string(err)}</p>
<% end %>
<% end %>
<%= for err <- upload_errors(@uploads.theme_logo_upload) do %>
<p class="theme-error-text">{error_to_string(err)}</p>
<% end %>
<%= if @logo_image do %>
<form
phx-change={@event_prefix <> "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>
<%= if @logo_image.is_svg do %>
<div class="theme-subfield">
<label class="admin-check-label">
<input
type="checkbox"
checked={@theme_settings.logo_recolor}
phx-click={@event_prefix <> "update_setting"}
phx-value-field="logo_recolor"
phx-value-setting_value={if @theme_settings.logo_recolor, do: "false", else: "true"}
class="admin-checkbox admin-checkbox-sm"
/>
<span class="theme-check-text">Recolour logo</span>
</label>
<%= if @theme_settings.logo_recolor do %>
<form
id="logo-color-form-compact"
phx-change={@event_prefix <> "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
# Header upload sub-section
attr :uploads, :map, required: true
attr :header_image, :map, default: nil
attr :theme_settings, :map, required: true
attr :contrast_warning, :atom, default: :ok
attr :event_prefix, :string, default: "theme_"
defp header_upload_section(assigns) do
~H"""
<div class="theme-subsection">
<span class="theme-slider-label theme-block-label">Upload header image</span>
<form phx-change="noop" phx-submit="noop">
<label class="theme-upload-label theme-upload-label-compact">
<span>Choose file...</span>
<.live_file_input upload={@uploads.theme_header_upload} class="hidden" />
</label>
</form>
<%= if @header_image do %>
<div class="theme-thumb theme-thumb-cover theme-thumb-header theme-thumb-compact">
<img src={"/image_cache/#{@header_image.id}.webp"} alt="" />
<button
type="button"
phx-click={@event_prefix <> "remove_header"}
class="theme-remove-btn"
title="Remove header background"
>
×
</button>
</div>
<%= if @contrast_warning != :ok do %>
<div class="theme-contrast-warning theme-contrast-warning-compact">
<strong>
<%= if @contrast_warning == :poor do %>
Text may be hard to read
<% else %>
Text contrast could be better
<% end %>
</strong>
<p>
Try switching to a
<%= if @theme_settings.mood == "dark" do %>
lighter mood
<% else %>
dark mood
<% end %>
or choosing a different image.
</p>
</div>
<% end %>
<div class="admin-stack admin-stack-md theme-subfield">
<form phx-change={@event_prefix <> "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={@event_prefix <> "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={@event_prefix <> "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.theme_header_upload.entries do %>
<.upload_progress entry={entry} upload_name="theme_header_upload" />
<%= for err <- upload_errors(@uploads.theme_header_upload, entry) do %>
<p class="theme-error-text">{error_to_string(err)}</p>
<% end %>
<% end %>
<%= for err <- upload_errors(@uploads.theme_header_upload) do %>
<p class="theme-error-text">{error_to_string(err)}</p>
<% end %>
</div>
"""
end
# Shared upload progress component
attr :entry, :map, required: true
attr :upload_name, :string, required: true
defp upload_progress(assigns) do
~H"""
<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="theme_cancel_upload"
phx-value-ref={@entry.ref}
phx-value-upload={@upload_name}
class="theme-upload-cancel"
>
×
</button>
</div>
"""
end
defp error_to_string(:too_large), do: "File is too large"
defp error_to_string(:too_many_files), do: "Too many files"
defp error_to_string(:not_accepted), do: "File type not accepted"
defp error_to_string(err), do: inspect(err)
# ── Full Customise Accordion ─────────────────────────────────────── # ── Full Customise Accordion ───────────────────────────────────────
# Advanced settings groups for admin theme page. # Advanced settings groups for admin theme page.

View File

@ -1127,6 +1127,63 @@ defmodule BerrypodWeb.PageEditorHook do
{:halt, socket} {:halt, socket}
end end
# ── Site branding events (route to theme state) ─────────────────────
# Handle shop name and theme branding settings from Site tab
defp handle_site_action("update_branding", %{"field" => "site_name"} = params, socket) do
# Site name updates Settings immediately (same as theme tab)
value = params["site_name"]
if value do
Settings.put_setting("site_name", value, "string")
{:halt, assign(socket, :site_name, value)}
else
{:halt, socket}
end
end
defp handle_site_action("update_branding", %{"field" => field} = params, socket) do
# Route branding setting changes to theme state
value = params[field] || params["setting_value"]
if value do
field_atom = String.to_existing_atom(field)
update_theme_setting(socket, %{field_atom => value}, field)
else
{:halt, socket}
end
end
defp handle_site_action("toggle_branding", %{"field" => field}, socket) do
# Route toggle to theme state (same logic as theme_toggle_setting)
field_atom = String.to_existing_atom(field)
current_value = Map.get(socket.assigns.theme_editor_settings, field_atom)
new_value = !current_value
# Prevent turning off show_site_name when there's no logo
if field_atom == :show_site_name && new_value == false && !has_valid_logo?(socket) do
{:halt, socket}
else
update_theme_setting(socket, %{field_atom => new_value}, field)
end
end
defp handle_site_action("update_color", %{"field" => field, "value" => value}, socket) do
# Route color updates to theme state
field_atom = String.to_existing_atom(field)
update_theme_setting(socket, %{field_atom => value}, field)
end
defp handle_site_action("remove_logo", _params, socket) do
# Delegate to theme action handler
handle_theme_action("remove_logo", %{}, socket)
end
defp handle_site_action("remove_header", _params, socket) do
# Delegate to theme action handler
handle_theme_action("remove_header", %{}, socket)
end
# Catch-all for unknown site actions # Catch-all for unknown site actions
defp handle_site_action(_action, _params, socket), do: {:halt, socket} defp handle_site_action(_action, _params, socket), do: {:halt, socket}

View File

@ -226,6 +226,13 @@ defmodule BerrypodWeb.PageRenderer do
<BerrypodWeb.ShopComponents.SiteEditor.site_editor <BerrypodWeb.ShopComponents.SiteEditor.site_editor
site_state={@site_state} site_state={@site_state}
site_nav_pages={@site_nav_pages} site_nav_pages={@site_nav_pages}
site_name={@site_name}
theme_settings={@theme_editor_settings}
logo_image={@theme_editor_logo_image}
header_image={@theme_editor_header_image}
icon_image={@theme_editor_icon_image}
contrast_warning={@theme_editor_contrast_warning}
uploads={@uploads}
/> />
""" """
end end