berrypod/docs/plans/unified-editing-mode.md
jamey 168b6ce76f
All checks were successful
deploy / deploy (push) Successful in 1m10s
implement unified on-site editor phases 1-2
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>
2026-03-09 09:01:21 +00:00

9.1 KiB
Raw Permalink Blame History

Unified On-Site Editing Mode

Status: In Progress (Phase 1-2 complete)

Replace the separate admin theme editor (/admin/theme) with an on-site editing experience. When the admin clicks "Theme" or "Edit page", they're taken to the actual shop where a sliding panel allows them to edit either page content or theme settings — seeing changes live on the real site.

Current Architecture

The codebase already has a robust pattern for on-site editing via the PageEditorHook:

  • PageEditorHook attaches to all shop pages, initializing ~15 editor-related assigns
  • editor_sheet component in ShopComponents.Layout renders a FAB + sliding panel
  • PageRenderer conditionally renders editor UI when @is_admin is true
  • Changes preview instantly; state persists across the session

The theme editor currently lives at /admin/theme with its own LiveView and fake preview.

Approach

Extend the existing editor infrastructure to support both page editing and theme editing:

  1. Add theme editing state to the existing hook system
  2. Add a tab switcher to the editor panel (Page | Theme | Settings)
  3. Extract theme editor UI into a reusable component
  4. Route /admin/theme to shop homepage with ?edit=theme param

User Flow

  1. Admin clicks "Theme" in admin sidebar → redirects to /?edit=theme
  2. Shop homepage loads with theme editing panel open
  3. Admin can switch between "Page", "Theme", and "Settings" tabs
  4. Admin can navigate to other shop pages while keeping editor open
  5. "Done" or "×" button returns to admin or closes panel

Files to Modify

File Change
lib/berrypod_web/page_editor_hook.ex Add theme/settings editing assigns, tab state, event handlers
lib/berrypod_web/page_renderer.ex Add tab UI to editor_sheet, dispatch to theme/page/settings content
lib/berrypod_web/components/shop_components/layout.ex Update editor_sheet for 3-tab support, add new keys to layout_keys
lib/berrypod_web/components/shop_components/theme_editor.ex New - Extracted theme editor panel component
lib/berrypod_web/components/shop_components/settings_editor.ex New - Page/shop settings panel component
lib/berrypod_web/router.ex Redirect /admin/theme to /?edit=theme
assets/css/shop/editor.css New - Editor panel styling (extract from admin)
assets/js/hooks/theme_editor.js New - ColorSync and other theme editor hooks

Implementation

Phase 1: Theme Editor Hook Integration

Add theme editing state to PageEditorHook:

# New assigns in on_mount
:theme_editing -> false
:theme_settings -> nil (loaded when theme_editing activates)
:theme_active_preset -> nil
:theme_logo_image -> nil
:theme_header_image -> nil
:theme_icon_image -> nil
:theme_contrast_warning -> :ok
:theme_customise_open -> false

# New event handlers
"theme_*" events -> handled by attach_hook similar to editor_* events

Phase 2: Editor Panel Tabs

Add three tabs to the editor panel:

Tab Content When available
Page Block editor for custom pages Only on editable pages (home, about, custom CMS pages)
Theme Global styles, mood, typography, branding Always
Settings Page-specific settings (SEO, slug) or global shop settings Always (content varies by page)

Update editor_sheet component:

<div class="editor-tabs">
  <button
    class={["editor-tab", @editor_active_tab == :page && "active"]}
    phx-click="editor_set_tab"
    phx-value-tab="page"
    disabled={!@page}
  >
    Page
  </button>
  <button
    class={["editor-tab", @editor_active_tab == :theme && "active"]}
    phx-click="editor_set_tab"
    phx-value-tab="theme"
  >
    Theme
  </button>
  <button
    class={["editor-tab", @editor_active_tab == :settings && "active"]}
    phx-click="editor_set_tab"
    phx-value-tab="settings"
  >
    Settings
  </button>
</div>

<%= case @editor_active_tab do %>
  <% :page -> %>
    <.editor_sheet_content ... />
  <% :theme -> %>
    <.theme_editor_content ... />
  <% :settings -> %>
    <.settings_editor_content ... />
<% end %>

Settings tab content varies by context:

  • On custom CMS pages: Page title, slug, SEO (meta title, description)
  • On product pages: Read-only product info, link to admin product editor
  • On collection pages: Collection settings, link to admin
  • Global fallback: Shop name, description, favicon settings

Phase 3: Extract Theme Editor Component

Create lib/berrypod_web/components/shop_components/theme_editor.ex:

  • Extract the sidebar content from current /admin/theme template
  • Reuse same event names (update_setting, toggle_setting, apply_preset, etc.)
  • Support image uploads via allow_upload passed from hook

Key sections to extract:

  • Presets grid
  • Mood/typography/shape/density selectors
  • Logo upload + settings
  • Header background upload + settings
  • Accent color pickers
  • Customise accordion (advanced settings)

Phase 3b: Create Settings Editor Component

Create lib/berrypod_web/components/shop_components/settings_editor.ex:

Content varies by page type:

Page type Settings shown
Custom CMS page Page title, slug, visibility, SEO meta
Home page Shop name, description, SEO defaults
Product page Read-only product name, link to admin product editor
Collection page Read-only collection name, link to admin
Cart/checkout Shop policies, checkout settings link

Common sections:

  • Page SEO (meta title, description, social image)
  • Favicon settings (short name, icon upload)
  • Shop identity (name, description)

Phase 4: Image Upload Handling

Image uploads need special handling since we're in a hook, not a LiveView:

Option A: Delegate to the parent LiveView

  • Pass @uploads from the LiveView through assigns
  • Hook events trigger upload handling in the LiveView
  • Cleanest approach, matches existing pattern

Option B: Use a LiveComponent for uploads

  • Wrap upload sections in a stateful LiveComponent
  • Component handles its own uploads
  • More isolated but adds complexity

Recommended: Option A — follow the pattern the page editor already uses.

Phase 5: URL-Based Mode Activation

Handle ?edit=theme query param:

# In PageEditorHook handle_params hook
defp handle_params_hook(params, _uri, socket) do
  socket =
    case params["edit"] do
      "theme" -> enter_theme_edit_mode(socket)
      "page" -> enter_page_edit_mode(socket)
      _ -> socket
    end
  {:cont, socket}
end

Phase 6: Admin Routing

Update router to redirect theme link:

# In admin scope, redirect /admin/theme to shop
get "/theme", Plug.Redirect, to: "/?edit=theme"

Or keep the admin theme page as a fallback/redirect:

live "/theme", Admin.Theme.Redirect  # Simple LiveView that redirects

State Management

Theme editing state persists across navigation via hook attach:

  • attach_hook(:theme_params, :handle_params, &handle_params_hook/3) tracks URL
  • Theme settings stored in socket assigns
  • CSS regenerated and assigned to @generated_css on each change
  • ThemeHook already loads CSS; we just update the assign

Dirty tracking:

  • Theme changes save immediately (current behavior) OR
  • Add "unsaved" state like page editor (better UX but more work)

Recommendation: Keep immediate save for now (it's the existing behavior and simpler).

CSS Considerations

The editor panel uses admin CSS classes. Options:

  1. Extract shared styles into shop/editor.css loaded on all shop pages
  2. Namespace admin styles so they work in shop context
  3. Use inline styles (not recommended)

Recommended: Create assets/css/shop/editor.css with editor panel styles, include it in shop layout when @is_admin.

What Happens to /admin/theme?

Options:

  1. Remove it entirely — redirects to /?edit=theme
  2. Keep as fallback — simple page that redirects or links to on-site editor
  3. Keep for non-visual settings — things like favicon short name, SEO defaults

Recommended: Redirect to /?edit=theme for now. Can always add back a minimal settings page later if needed.

Verification

  1. Click "Theme" in admin → lands on shop homepage with theme panel open
  2. Change mood → see colors update instantly on real site
  3. Upload logo → appears in real header immediately
  4. Navigate to product page → theme panel stays open
  5. Switch to "Page" tab on homepage → shows page block editor
  6. Switch to "Page" tab on product page → tab disabled (no editable blocks)
  7. Click "Done" → returns to admin dashboard
  8. Mobile: panel slides up from bottom, fully usable

Phased Rollout

MVP (Phases 1-3)

  • Theme editing works on-site
  • Basic tab UI (Page | Theme | Settings)
  • All current theme settings functional
  • No image uploads yet (use existing images)

Full Feature (Phases 4-6)

  • Image uploads working
  • URL-based mode activation
  • Admin redirect in place
  • Polish and edge cases

Future Enhancements (Not in Scope)

  • Undo/redo for theme changes
  • "Preview without saving" mode
  • Theme presets preview before applying
  • Multi-page preview from single location