# 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 <.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 ` ``` **Right:** Click handler on the close button ```heex

{msg}

```