implement unified on-site editor phases 1-2
All checks were successful
deploy / deploy (push) Successful in 1m10s

Add theme editing to the existing PageEditorHook, enabling on-site
theme customisation alongside page editing. The editor panel now has
three tabs (Page, Theme, Settings) and can be collapsed while
keeping editing state intact.

- Add theme editing state and event handlers to PageEditorHook
- Add 3-tab UI with tab switching logic
- Add transparent overlay for click-outside dismiss
- Add mobile drag-to-resize with height persistence
- Fix animation replay on drag release (has-dragged class)
- Preserve panel height across LiveView re-renders
- Default to Page tab on editable pages, Theme otherwise
- Show unsaved changes indicator on FAB when panel collapsed
- Fix handle_event grouping warning in admin theme

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-03-09 09:01:21 +00:00
parent 74ab6411f7
commit 168b6ce76f
10 changed files with 954 additions and 53 deletions

View File

@@ -81,14 +81,18 @@ defmodule BerrypodWeb.PageRenderer do
</main>
</.shop_layout>
<%!-- Editor sheet for page editing --%>
<%!-- Editor sheet for page/theme/settings editing --%>
<.editor_sheet
editing={@editing}
theme_editing={Map.get(assigns, :theme_editing, false)}
editor_dirty={@editor_dirty}
editor_sheet_state={assigns[:editor_sheet_state] || :collapsed}
editor_save_status={@editor_save_status}
editor_active_tab={Map.get(assigns, :editor_active_tab, :page)}
has_editable_page={@page != nil}
>
<.editor_sheet_content
<.editor_panel_content
editor_active_tab={Map.get(assigns, :editor_active_tab, :page)}
page={@page}
editing_blocks={@editing_blocks}
editor_history={@editor_history}
@@ -103,13 +107,247 @@ defmodule BerrypodWeb.PageRenderer do
editor_image_picker_images={@editor_image_picker_images}
editor_image_picker_search={@editor_image_picker_search}
editor_at_defaults={Map.get(assigns, :editor_at_defaults, true)}
theme_editor_settings={Map.get(assigns, :theme_editor_settings)}
theme_editor_active_preset={Map.get(assigns, :theme_editor_active_preset)}
theme_editor_presets={Map.get(assigns, :theme_editor_presets, [])}
theme_editor_customise_open={Map.get(assigns, :theme_editor_customise_open, false)}
site_name={Map.get(assigns, :site_name, "")}
/>
</.editor_sheet>
"""
end
# Editor panel content dispatcher - shows content based on active tab
attr :editor_active_tab, :atom, default: :page
attr :page, :map, default: nil
attr :editing_blocks, :list, default: nil
attr :editor_history, :list, default: []
attr :editor_future, :list, default: []
attr :editor_dirty, :boolean, default: false
attr :editor_live_region_message, :string, default: nil
attr :editor_expanded, :any, default: nil
attr :editor_show_picker, :boolean, default: false
attr :editor_picker_filter, :string, default: ""
attr :editor_allowed_blocks, :list, default: nil
attr :editor_image_picker_block_id, :string, default: nil
attr :editor_image_picker_images, :list, default: []
attr :editor_image_picker_search, :string, default: ""
attr :editor_at_defaults, :boolean, default: true
attr :theme_editor_settings, :map, default: nil
attr :theme_editor_active_preset, :atom, default: nil
attr :theme_editor_presets, :list, default: []
attr :theme_editor_customise_open, :boolean, default: false
attr :site_name, :string, default: ""
defp editor_panel_content(%{editor_active_tab: :page} = assigns) do
~H"""
<.editor_sheet_content
page={@page}
editing_blocks={@editing_blocks}
editor_history={@editor_history}
editor_future={@editor_future}
editor_dirty={@editor_dirty}
editor_live_region_message={@editor_live_region_message}
editor_expanded={@editor_expanded}
editor_show_picker={@editor_show_picker}
editor_picker_filter={@editor_picker_filter}
editor_allowed_blocks={@editor_allowed_blocks}
editor_image_picker_block_id={@editor_image_picker_block_id}
editor_image_picker_images={@editor_image_picker_images}
editor_image_picker_search={@editor_image_picker_search}
editor_at_defaults={@editor_at_defaults}
/>
"""
end
defp editor_panel_content(%{editor_active_tab: :theme} = assigns) do
~H"""
<.theme_editor_content
theme_editor_settings={@theme_editor_settings}
theme_editor_active_preset={@theme_editor_active_preset}
theme_editor_presets={@theme_editor_presets}
theme_editor_customise_open={@theme_editor_customise_open}
site_name={@site_name}
/>
"""
end
defp editor_panel_content(%{editor_active_tab: :settings} = assigns) do
~H"""
<.settings_editor_content page={@page} site_name={@site_name} />
"""
end
# Theme editor content - shows theme controls
attr :theme_editor_settings, :map, default: nil
attr :theme_editor_active_preset, :atom, default: nil
attr :theme_editor_presets, :list, default: []
attr :theme_editor_customise_open, :boolean, default: false
attr :site_name, :string, default: ""
defp theme_editor_content(assigns) do
~H"""
<div class="editor-theme-content">
<%= if @theme_editor_settings do %>
<%!-- Shop name --%>
<div class="theme-section">
<label class="theme-section-label">Shop name</label>
<form phx-change="theme_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>
<%!-- Presets --%>
<div class="theme-section">
<label class="theme-section-label">Preset</label>
<div class="theme-presets">
<%= for {preset_name, description} <- @theme_editor_presets do %>
<button
type="button"
phx-click="theme_apply_preset"
phx-value-preset={preset_name}
class={[
"theme-preset",
@theme_editor_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>
<%!-- Mood --%>
<div class="theme-section">
<label class="theme-section-label">Colour mood</label>
<div class="theme-chips">
<%= for mood <- ["warm", "neutral", "cool", "dark"] do %>
<button
type="button"
phx-click="theme_update_setting"
phx-value-field="mood"
phx-value-setting_value={mood}
class={["theme-chip", @theme_editor_settings.mood == mood && "theme-chip-active"]}
>
{mood}
</button>
<% end %>
</div>
</div>
<%!-- Typography --%>
<div class="theme-section">
<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="theme_update_setting"
phx-value-field="typography"
phx-value-setting_value={typo}
class={[
"theme-chip",
@theme_editor_settings.typography == typo && "theme-chip-active"
]}
>
{typo}
</button>
<% end %>
</div>
</div>
<%!-- Shape --%>
<div class="theme-section">
<label class="theme-section-label">Corner style</label>
<div class="theme-chips">
<%= for shape <- ["sharp", "soft", "round", "pill"] do %>
<button
type="button"
phx-click="theme_update_setting"
phx-value-field="shape"
phx-value-setting_value={shape}
class={["theme-chip", @theme_editor_settings.shape == shape && "theme-chip-active"]}
>
{shape}
</button>
<% end %>
</div>
</div>
<%!-- More options link --%>
<details
class="theme-customise"
id="theme-customise-section"
open={@theme_editor_customise_open}
>
<summary class="theme-customise-summary" phx-click="theme_toggle_customise">
<span class="theme-customise-label">More options</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">
<p class="admin-text-secondary">
For full theme customisation including branding, colours, and layout, <a
href="/admin/theme"
class="admin-link"
>visit the theme editor</a>.
</p>
</div>
</details>
<% else %>
<p class="admin-text-secondary">Loading theme settings...</p>
<% end %>
</div>
"""
end
# Settings editor content - shows page/shop settings
attr :page, :map, default: nil
attr :site_name, :string, default: ""
defp settings_editor_content(assigns) do
~H"""
<div class="editor-settings-content">
<%= if @page do %>
<div class="theme-section">
<label class="theme-section-label">Page</label>
<p class="admin-text-secondary">{@page.title}</p>
</div>
<div class="theme-section">
<p class="admin-text-secondary">
Page settings like SEO, visibility, and slug editing coming soon.
For now, <a href="/admin/pages" class="admin-link">manage pages in admin</a>.
</p>
</div>
<% else %>
<div class="theme-section">
<p class="admin-text-secondary">
This page doesn't have editable settings.
<a href="/admin/settings" class="admin-link">Shop settings</a>
can be changed in admin.
</p>
</div>
<% end %>
</div>
"""
end
# Editor sheet content - the block list and editing controls
attr :page, :map, required: true
attr :page, :map, default: nil
attr :editing_blocks, :list, default: nil
attr :editor_history, :list, default: []
attr :editor_future, :list, default: []