# Notification system

Status: Complete

This document describes the notification and feedback patterns used throughout the app. Follow these rules when implementing new features or modifying existing ones.

## Core principles

1. **No floating toasts** — fixed-position overlays are removed entirely
2. **Inline feedback for form saves** — contextual, next to the action
3. **Banners for page-level outcomes** — document flow, persistent until dismissed
4. **Progressive enhancement** — works without JS, enhanced with LiveView

## Components

### `<.inline_feedback>` — contextual save status

Used for form saves that stay on the same page. Shows status next to the save button.

```heex
<.button type="submit">Save</.button>
<.inline_feedback status={@save_status} />
```

**States:**
- `:idle` — hidden
- `:saving` — spinner + "Saving..."
- `:saved` — green tick + "Saved"
- `:error` — red icon + "Something went wrong" (or custom message)

**Implementation pattern:**

```elixir
# Mount
assign(socket, :save_status, :idle)

# Validate event — clear status on form change
def handle_event("validate", params, socket) do
  {:noreply, assign(socket, save_status: :idle)}
end

# Save event
def handle_event("save", params, socket) do
  case Context.save(params) do
    {:ok, _} -> {:noreply, assign(socket, :save_status, :saved)}
    {:error, _} -> {:noreply, assign(socket, :save_status, :error)}
  end
end
```

**When to use:** Theme editor, page editor, settings forms, media metadata, product details, navigation editor, newsletter drafts.

### `<.flash>` / `<.shop_flash_group>` — page-level banners

Used for outcomes that affect the whole page or result from navigation. Renders in document flow (pushes content down), not fixed overlay.

```elixir
socket
|> put_flash(:info, "Product sync started")
|> push_navigate(to: ~p"/admin/products")
```

**Behaviour:**
- Persistent until dismissed (close button)
- Clears automatically on next navigation
- Info messages use `role="status"` + `aria-live="polite"`
- Error messages use `role="alert"` + `aria-live="assertive"`

**When to use:**
- Operation outcomes: "Sync started", "Page deleted", "Campaign sent"
- Auth messages: "Welcome back!", "Logged out"
- Connection/provider status changes
- Any action that navigates to a new page

## Decision tree

```
Is this feedback for a form save that stays on the page?
├─ YES → Use inline_feedback
│        - Add :save_status assign
│        - Clear to :idle on validate
│        - Set to :saved/:error on submit
│
└─ NO → Does the action navigate away or affect the whole page?
        ├─ YES → Use put_flash (banner)
        │        - :info for success
        │        - :error for failures
        │
        └─ NO → Use inline_feedback anyway
                (contextual is usually better)
```

## Progressive enhancement

Forms should work without JavaScript. The pattern:

1. Form has both `action` (controller POST) and `phx-submit` (LiveView)
2. With JS: LiveView handles the event, shows inline feedback
3. Without JS: Form POSTs to controller, redirects with flash

```heex
<.form
  for={@form}
  action={~p"/admin/settings/email"}
  method="post"
  phx-change="validate"
  phx-submit="save"
>
```

The controller sets a flash and redirects:

```elixir
def update(conn, params) do
  case Context.save(params) do
    {:ok, _} ->
      conn
      |> put_flash(:info, "Settings saved")
      |> redirect(to: ~p"/admin/settings")
    {:error, changeset} ->
      render(conn, :edit, form: to_form(changeset))
  end
end
```

**Result:** With JS, user sees instant inline feedback. Without JS, user gets a banner after redirect.

## Accessibility requirements

All notification components must include:

1. **Appropriate role:**
   - `role="status"` for info/success (non-urgent)
   - `role="alert"` for errors (urgent)

2. **aria-live attribute:**
   - `aria-live="polite"` for info/success
   - `aria-live="assertive"` for errors

3. **Dismiss buttons:**
   - Must be actual `<button>` elements
   - Include `aria-label="Close"`
   - Icon must have `aria-hidden="true"`

## Files

- **Component:** `lib/berrypod_web/components/core_components.ex` — `inline_feedback/1`, `flash/1`
- **Shop flash:** `lib/berrypod_web/components/shop_components/content.ex` — `shop_flash_group/1`
- **Admin CSS:** `assets/css/admin/core.css` — `.admin-inline-feedback`, `.admin-banner`, `.admin-alert`
- **Shop CSS:** `assets/css/shop/components.css` — `.shop-flash`

## Examples in codebase

| Page | Feedback type | File |
|------|--------------|------|
| Theme editor | inline_feedback | `lib/berrypod_web/live/admin/theme/index.ex` |
| Page editor | inline_feedback | `lib/berrypod_web/live/admin/pages/editor.ex` |
| Settings | inline_feedback | `lib/berrypod_web/live/admin/settings.ex` |
| Email settings | inline_feedback | `lib/berrypod_web/live/admin/email_settings.ex` |
| Media library | inline_feedback | `lib/berrypod_web/live/admin/media.ex` |
| Product show | inline_feedback | `lib/berrypod_web/live/admin/product_show.ex` |
| Navigation | inline_feedback | `lib/berrypod_web/live/admin/navigation.ex` |
| Newsletter | inline_feedback | `lib/berrypod_web/live/admin/newsletter/campaign_form.ex` |
| Provider form | flash (navigates) | `lib/berrypod_web/live/admin/providers/form.ex` |
| Contact | flash (navigates) | `lib/berrypod_web/live/shop/contact.ex` |

## Common mistakes

**Wrong:** Using `put_flash` for a form save that stays on the page
```elixir
# Don't do this
def handle_event("save", params, socket) do
  {:ok, _} = Context.save(params)
  {:noreply, put_flash(socket, :info, "Saved")}
end
```

**Right:** Using inline feedback
```elixir
def handle_event("save", params, socket) do
  {:ok, _} = Context.save(params)
  {:noreply, assign(socket, :save_status, :saved)}
end
```

**Wrong:** Forgetting to clear status on validate
```elixir
# Status stays "Saved" even while user is editing
def handle_event("validate", params, socket) do
  {:noreply, assign(socket, :form, to_form(params))}
end
```

**Right:** Clear status when form changes
```elixir
def handle_event("validate", params, socket) do
  {:noreply, assign(socket, form: to_form(params), save_status: :idle)}
end
```

**Wrong:** Making flash the whole-div clickable
```heex
<div phx-click={dismiss()}>
  <p>{msg}</p>
  <button>×</button>  <!-- button does nothing -->
</div>
```

**Right:** Click handler on the close button
```heex
<div>
  <p>{msg}</p>
  <button phx-click={dismiss()} aria-label="Close">×</button>
</div>
```
