# Unified Editor Session

**Status:** Complete
**Depends on:** Editor panel reorganisation (Phase 1-2 complete)
**Estimated effort:** 4-5 hours

## Overview

Unify the editing experience across all three tabs (Page, Theme, Site) so they behave consistently:

- **Instant preview** — all changes show immediately on the shop
- **Explicit save** — changes only persist to database when you click Save
- **Free tab switching** — no warnings when switching between tabs
- **Accumulated changes** — edits across all tabs are saved together
- **Navigation warning** — warn on page navigation or refresh if any tab has unsaved changes

## Current State

| Tab | Dirty Tracking | Save Method | Preview |
|-----|----------------|-------------|---------|
| Page | `editor_dirty` | Manual Save | Instant (in-memory blocks) |
| Theme | None | Auto-save | Instant (CSS regenerated) |
| Site | None | Auto-save | Instant (assigns updated) |

## Target State

| Tab | Dirty Tracking | Save Method | Preview |
|-----|----------------|-------------|---------|
| Page | `page_dirty` | Unified Save | Instant (in-memory blocks) |
| Theme | `theme_dirty` | Unified Save | Instant (in-memory settings) |
| Site | `site_dirty` | Unified Save | Instant (in-memory settings) |

Unified dirty flag: `editor_dirty = page_dirty || theme_dirty || site_dirty`

## Implementation Tasks

### Phase 1: Theme Tab — Preview Without Auto-Save (1.5h)

Currently Theme auto-saves every change. We need to decouple preview from persistence.

#### 1.1 Add theme dirty tracking

In `page_editor_hook.ex`, add assigns:

```elixir
|> assign(:theme_dirty, false)
|> assign(:theme_editor_original, nil)  # snapshot at load time
```

#### 1.2 Modify theme event handlers

Change handlers like `theme_update_setting`, `theme_update_color`, etc. to:

1. Update `theme_editor_settings` in memory (for preview)
2. Regenerate CSS for live preview
3. Set `theme_dirty = true`
4. **Do NOT call `Settings.update_theme_settings()`**

#### 1.3 Store original theme state

When loading theme state in `load_theme_state/1`:

```elixir
|> assign(:theme_editor_original, theme_settings)
```

#### 1.4 Add theme revert logic

On discard/close with dirty state, restore from `theme_editor_original`.

### Phase 2: Site Tab — Preview Without Auto-Save (1h)

#### 2.1 Add site dirty tracking

```elixir
|> assign(:site_dirty, false)
|> assign(:site_editor_original, nil)
```

#### 2.2 Modify site event handlers

Change `handle_site_update/2` to:

1. Update `site_*` assigns in memory (for preview)
2. Set `site_dirty = true`
3. **Do NOT call `Site.put_announcement()` or `Site.put_footer_content()`**

#### 2.3 Store original site state

When loading site state in `load_site_state/1`:

```elixir
original = %{
  announcement_text: site_settings.announcement_text,
  announcement_link: site_settings.announcement_link,
  announcement_style: site_settings.announcement_style,
  footer_about: site_settings.footer_about,
  footer_copyright: site_settings.footer_copyright,
  show_newsletter: site_settings.show_newsletter
}
|> assign(:site_editor_original, original)
```

### Phase 3: Unified Save Button (1h)

#### 3.1 Compute unified dirty state

```elixir
defp compute_editor_dirty(socket) do
  page_dirty = socket.assigns[:editor_dirty] || false
  theme_dirty = socket.assigns[:theme_dirty] || false
  site_dirty = socket.assigns[:site_dirty] || false
  settings_dirty = socket.assigns[:settings_dirty] || false

  assign(socket, :any_dirty, page_dirty || theme_dirty || site_dirty || settings_dirty)
end
```

#### 3.2 Update Save button

In `editor_sheet` header, change Save button to:

- Enabled when `@any_dirty`
- On click: `editor_save_all` event

#### 3.3 Implement unified save handler

```elixir
defp handle_editor_event("editor_save_all", _params, socket) do
  socket = save_all_tabs(socket)
  {:halt, socket}
end

defp save_all_tabs(socket) do
  socket
  |> maybe_save_page()
  |> maybe_save_theme()
  |> maybe_save_site()
  |> maybe_save_settings()
  |> assign(:editor_save_status, :saved)
  |> schedule_save_status_clear()
end

defp maybe_save_page(socket) do
  if socket.assigns[:editor_dirty] do
    # existing page save logic
  else
    socket
  end
end

defp maybe_save_theme(socket) do
  if socket.assigns[:theme_dirty] do
    Settings.update_theme_settings(socket.assigns.theme_editor_settings)
    assign(socket, :theme_dirty, false)
  else
    socket
  end
end

defp maybe_save_site(socket) do
  if socket.assigns[:site_dirty] do
    site = socket.assigns
    Site.put_announcement(site.site_announcement_text, site.site_announcement_link, site.site_announcement_style)
    Site.put_footer_content(site.site_footer_about, site.site_footer_copyright, site.site_footer_show_newsletter)
    assign(socket, :site_dirty, false)
  else
    socket
  end
end
```

### Phase 4: Visual Indicators (0.5h)

#### 4.1 Dirty dots on tab labels

In `editor_sheet` tablist:

```heex
<button role="tab" ...>
  Page
  <span :if={@page_dirty} class="editor-tab-dirty-dot" aria-label="unsaved changes"></span>
</button>
```

CSS:
```css
.editor-tab-dirty-dot {
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--color-warning);
  margin-left: 4px;
}
```

#### 4.2 Update EditorKeyboard hook

Pass unified dirty state to the hook:

```heex
<div id="editor-sheet-inner"
     phx-hook="EditorKeyboard"
     data-dirty={to_string(@any_dirty)}>
```

### Phase 5: Navigation Warning Flow (1h)

#### 5.1 Update EditorKeyboard JS hook

Modify the navigation intercept in `assets/js/app.js`:

```javascript
// On navigation attempt when dirty
if (this.el.dataset.dirty === "true") {
  e.preventDefault();
  e.stopImmediatePropagation();

  // Send event to LiveView to show modal
  this.pushEvent("editor_nav_blocked", { href: href });
}
```

#### 5.2 Add navigation modal component

```heex
<dialog :if={@editor_nav_blocked} class="editor-nav-modal" open>
  <div class="editor-nav-modal-content">
    <h3>Unsaved changes</h3>
    <p>You have unsaved changes. What would you like to do?</p>
    <div class="editor-nav-modal-actions">
      <button phx-click="editor_save_and_navigate">Save & continue</button>
      <button phx-click="editor_discard_and_navigate">Discard & continue</button>
      <button phx-click="editor_cancel_navigate">Cancel</button>
    </div>
  </div>
</dialog>
```

#### 5.3 Handle modal events

```elixir
defp handle_editor_event("editor_nav_blocked", %{"href" => href}, socket) do
  {:halt, assign(socket, editor_nav_blocked: href)}
end

defp handle_editor_event("editor_save_and_navigate", _params, socket) do
  socket = save_all_tabs(socket)
  {:halt, push_navigate(socket, to: socket.assigns.editor_nav_blocked)}
end

defp handle_editor_event("editor_discard_and_navigate", _params, socket) do
  socket = revert_all_tabs(socket)
  {:halt, push_navigate(socket, to: socket.assigns.editor_nav_blocked)}
end

defp handle_editor_event("editor_cancel_navigate", _params, socket) do
  # Re-open sheet if collapsed
  socket =
    socket
    |> assign(:editor_nav_blocked, nil)
    |> assign(:editor_sheet_state, :open)
  {:halt, socket}
end
```

#### 5.4 Add revert logic

```elixir
defp revert_all_tabs(socket) do
  socket
  |> maybe_revert_page()
  |> maybe_revert_theme()
  |> maybe_revert_site()
  |> assign(:any_dirty, false)
end

defp maybe_revert_theme(socket) do
  if socket.assigns[:theme_dirty] do
    original = socket.assigns.theme_editor_original
    # Regenerate CSS from original
    # Reset theme_editor_settings to original
  else
    socket
  end
end
```

## Files to Modify

| File | Changes |
|------|---------|
| `lib/berrypod_web/page_editor_hook.ex` | Dirty tracking, save handlers, revert logic |
| `lib/berrypod_web/components/shop_components/layout.ex` | Tab dirty dots, Save button, nav modal |
| `lib/berrypod_web/page_renderer.ex` | Pass unified dirty state to editor_sheet |
| `assets/js/app.js` | EditorKeyboard nav intercept |
| `assets/css/admin/components.css` | Dirty dot styles, nav modal styles |

## Testing Plan

1. **Page tab**: Edit blocks → switch tabs → switch back → verify changes preserved
2. **Theme tab**: Change color → switch tabs → verify preview still shows new color
3. **Site tab**: Edit announcement → switch tabs → verify preview shows new text
4. **Save all**: Make changes in all tabs → click Save → verify all persisted
5. **Navigate away**: Make changes → click link → verify warning modal
6. **Save & continue**: Warning modal → Save & continue → verify saved and navigated
7. **Discard & continue**: Warning modal → Discard → verify reverted and navigated
8. **Cancel**: Warning modal → Cancel → verify sheet opens, changes preserved
9. **Browser refresh**: Make changes → F5 → verify beforeunload warning
10. **Tab close**: Make changes → close tab → verify beforeunload warning

## Rollback Plan

If issues arise, the immediate fix is to re-enable auto-save for Theme/Site tabs by calling the persistence functions in the event handlers again. The dirty tracking can remain in place.

## Future Enhancements

- **Undo/redo for Theme/Site**: Currently only Page tab has history
- **Draft persistence**: Save drafts to localStorage for recovery after crash
- **Keyboard shortcuts**: Cmd+S to save all
