implement unified on-site editor phases 1-2
All checks were successful
deploy / deploy (push) Successful in 1m10s
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:
@@ -48,10 +48,11 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
|
||||
# Keys accepted by shop_layout — used by layout_assigns/1 so page templates
|
||||
# can spread assigns without listing each one explicitly.
|
||||
@layout_keys ~w(theme_settings site_name logo_image header_image mode cart_items cart_count
|
||||
@layout_keys ~w(theme_settings generated_css site_name logo_image header_image mode cart_items cart_count
|
||||
cart_subtotal cart_total cart_drawer_open cart_status active_page error_page is_admin
|
||||
search_query search_results search_open categories shipping_estimate
|
||||
country_code available_countries editing editor_current_path editor_sidebar_open
|
||||
country_code available_countries editing theme_editing editor_current_path editor_sidebar_open
|
||||
editor_active_tab editor_sheet_state editor_dirty editor_save_status
|
||||
header_nav_items footer_nav_items newsletter_enabled newsletter_state stripe_connected)a
|
||||
|
||||
@doc """
|
||||
@@ -102,6 +103,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
attr :newsletter_enabled, :boolean, default: false
|
||||
attr :newsletter_state, :atom, default: :idle
|
||||
attr :stripe_connected, :boolean, default: true
|
||||
attr :generated_css, :string, default: nil
|
||||
|
||||
slot :inner_block, required: true
|
||||
|
||||
@@ -110,9 +112,24 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
<div
|
||||
id={unless @error_page, do: "shop-container"}
|
||||
phx-hook={unless @error_page, do: "CartPersist"}
|
||||
class="shop-container"
|
||||
class="shop-container themed"
|
||||
data-bottom-nav={!@error_page || nil}
|
||||
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}
|
||||
>
|
||||
<%!-- Live-updatable theme CSS (overrides static version in head) --%>
|
||||
<%= if @generated_css do %>
|
||||
{Phoenix.HTML.raw("<style id=\"theme-css-live\">#{@generated_css}</style>")}
|
||||
<% end %>
|
||||
|
||||
<.skip_link />
|
||||
|
||||
<%= if @theme_settings.announcement_bar do %>
|
||||
@@ -1032,7 +1049,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
# ── Editor sheet ────────────────────────────────────────────────────
|
||||
|
||||
@doc """
|
||||
Renders the unified editor sheet for page editing.
|
||||
Renders the unified editor sheet for page/theme/settings editing.
|
||||
|
||||
The sheet is anchored to the bottom edge on mobile (<768px) and the right edge
|
||||
on desktop (≥768px). It has three states on mobile (collapsed, partial, full)
|
||||
@@ -1040,59 +1057,98 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
|
||||
## Attributes
|
||||
|
||||
* `editing` - Whether edit mode is active.
|
||||
* `editor_dirty` - Whether there are unsaved changes.
|
||||
* `editing` - Whether page edit mode is active.
|
||||
* `theme_editing` - Whether theme edit mode is active.
|
||||
* `editor_dirty` - Whether there are unsaved page changes.
|
||||
* `editor_sheet_state` - Current state (:collapsed, :partial, :full, or :open).
|
||||
* `editor_active_tab` - Current tab (:page, :theme, :settings).
|
||||
* `has_editable_page` - Whether the current page has editable blocks.
|
||||
|
||||
## Slots
|
||||
|
||||
* `inner_block` - The editor content (block list, settings, etc.).
|
||||
"""
|
||||
attr :editing, :boolean, default: false
|
||||
attr :theme_editing, :boolean, default: false
|
||||
attr :editor_dirty, :boolean, default: false
|
||||
attr :editor_sheet_state, :atom, default: :collapsed
|
||||
attr :editor_save_status, :atom, default: :idle
|
||||
attr :editor_active_tab, :atom, default: :page
|
||||
attr :has_editable_page, :boolean, default: false
|
||||
|
||||
slot :inner_block
|
||||
|
||||
def editor_sheet(assigns) do
|
||||
# Determine panel title based on active tab
|
||||
title =
|
||||
case assigns.editor_active_tab do
|
||||
:page -> "Page"
|
||||
:theme -> "Theme"
|
||||
:settings -> "Settings"
|
||||
end
|
||||
|
||||
# Any editing mode active
|
||||
any_editing = assigns.editing || assigns.theme_editing
|
||||
|
||||
assigns =
|
||||
assigns
|
||||
|> assign(:title, title)
|
||||
|> assign(:any_editing, any_editing)
|
||||
|
||||
~H"""
|
||||
<%!-- Floating action button: always visible when panel is closed --%>
|
||||
<button
|
||||
:if={@editor_sheet_state == :collapsed}
|
||||
type="button"
|
||||
phx-click={if @editing, do: "editor_set_sheet_state", else: "editor_toggle_editing"}
|
||||
phx-value-state={if @editing, do: "open", else: nil}
|
||||
phx-click={if @any_editing, do: "editor_set_sheet_state", else: "editor_set_tab"}
|
||||
phx-value-state={if @any_editing, do: "open", else: nil}
|
||||
phx-value-tab={
|
||||
if @any_editing, do: nil, else: if(@has_editable_page, do: "page", else: "theme")
|
||||
}
|
||||
class="editor-fab"
|
||||
aria-label={if @editing, do: "Show editor", else: "Edit page"}
|
||||
aria-label={if @any_editing, do: "Show editor", else: "Edit"}
|
||||
>
|
||||
<.edit_pencil_svg />
|
||||
<span>{if @editing, do: "Show editor", else: "Edit page"}</span>
|
||||
<span>{if @any_editing, do: "Show editor", else: "Edit"}</span>
|
||||
<span :if={@editing && @editor_dirty} class="editor-fab-dirty" aria-label="Unsaved changes" />
|
||||
</button>
|
||||
|
||||
<%!-- Overlay to catch taps outside the panel --%>
|
||||
<div
|
||||
:if={@editor_sheet_state == :open}
|
||||
class="editor-overlay"
|
||||
phx-click="editor_set_sheet_state"
|
||||
phx-value-state="collapsed"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<%!-- Editor panel: slides in/out --%>
|
||||
<aside
|
||||
id="editor-panel"
|
||||
class="editor-panel"
|
||||
role="region"
|
||||
aria-label="Page editor"
|
||||
aria-label="Site editor"
|
||||
aria-hidden={to_string(@editor_sheet_state == :collapsed)}
|
||||
data-state={@editor_sheet_state}
|
||||
data-editing={to_string(@editing)}
|
||||
data-editing={to_string(@any_editing)}
|
||||
phx-hook="EditorSheet"
|
||||
>
|
||||
<%!-- Drag handle for mobile resizing --%>
|
||||
<div class="editor-panel-drag-handle" data-drag-handle>
|
||||
<div class="editor-panel-drag-handle-bar" />
|
||||
</div>
|
||||
|
||||
<div class="editor-panel-header">
|
||||
<div class="editor-panel-header-left">
|
||||
<span class="editor-panel-title">Page editor</span>
|
||||
<span :if={@editor_dirty} class="editor-panel-dirty" aria-live="polite">
|
||||
<span class="editor-panel-title">{@title}</span>
|
||||
<span :if={@editing && @editor_dirty} class="editor-panel-dirty" aria-live="polite">
|
||||
<span class="editor-panel-dirty-dot" aria-hidden="true" />
|
||||
<span>Unsaved</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="editor-panel-header-actions">
|
||||
<button
|
||||
:if={@editor_save_status == :saved}
|
||||
:if={@editor_active_tab == :page && @editor_save_status == :saved}
|
||||
type="button"
|
||||
class="admin-btn admin-btn-sm admin-btn-ghost"
|
||||
disabled
|
||||
@@ -1100,7 +1156,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
Saved ✓
|
||||
</button>
|
||||
<button
|
||||
:if={@editor_save_status != :saved}
|
||||
:if={@editor_active_tab == :page && @editor_save_status != :saved}
|
||||
type="button"
|
||||
phx-click="editor_save"
|
||||
class={["admin-btn admin-btn-sm", @editor_dirty && "admin-btn-primary"]}
|
||||
@@ -1108,9 +1164,66 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
phx-click="editor_set_sheet_state"
|
||||
phx-value-state="collapsed"
|
||||
class="editor-panel-close"
|
||||
aria-label="Close editor"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
width="20"
|
||||
height="20"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%!-- Tab bar --%>
|
||||
<div class="editor-tabs" role="tablist">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
phx-click="editor_set_tab"
|
||||
phx-value-tab="page"
|
||||
class={["editor-tab", @editor_active_tab == :page && "editor-tab-active"]}
|
||||
aria-selected={to_string(@editor_active_tab == :page)}
|
||||
disabled={!@has_editable_page}
|
||||
title={
|
||||
if @has_editable_page, do: "Edit page blocks", else: "This page has no editable blocks"
|
||||
}
|
||||
>
|
||||
Page
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
phx-click="editor_set_tab"
|
||||
phx-value-tab="theme"
|
||||
class={["editor-tab", @editor_active_tab == :theme && "editor-tab-active"]}
|
||||
aria-selected={to_string(@editor_active_tab == :theme)}
|
||||
>
|
||||
Theme
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
phx-click="editor_set_tab"
|
||||
phx-value-tab="settings"
|
||||
class={["editor-tab", @editor_active_tab == :settings && "editor-tab-active"]}
|
||||
aria-selected={to_string(@editor_active_tab == :settings)}
|
||||
>
|
||||
Settings
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="editor-panel-content">
|
||||
{render_slot(@inner_block)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user