diff --git a/docs/plans/notification-overhaul.md b/docs/plans/notification-overhaul.md index bd0ae9a..91e2ba6 100644 --- a/docs/plans/notification-overhaul.md +++ b/docs/plans/notification-overhaul.md @@ -6,138 +6,317 @@ Replace floating toast/flash notifications with inline feedback and persistent t ## Current state -Phoenix flash messages shown as floating toasts. 110+ flash messages across 28 files (65 info, 45+ error). Auto-dismiss after a few seconds. Overlay existing UI content. +144 `put_flash` calls across the codebase (59 `:info`, 34 `:error`, rest multi-line). Two separate toast implementations: + +- **Admin** (`core_components.ex` → `.admin-toast`): fixed top-right, 20rem wide, click to dismiss via `JS.push("lv:clear-flash")` + `JS.hide` +- **Shop** (`shop_components/content.ex` → `.shop-flash`): fixed top-right, theme-aware, CSS slide-in, click anywhere to dismiss + +Both are overlays — fixed-position, outside document flow, disappear on click. + +**Existing inline patterns** already in the codebase: +- `<.error>` component — red text below form fields (`.admin-error`) via changeset errors +- Static `.admin-alert` boxes — persistent info/error in auth/setup pages +- `.admin-checklist-banner` — contextual "back to checklist" banners +- `.admin-banner-warning` — full-width warning banner (email settings) + +**Top files by flash count:** settings.ex (17), onboarding.ex (9), campaign_form.ex (9), pages/editor.ex (8), theme/index.ex (6), media.ex (6) ## New approach: two layers -### 1. Inline feedback (primary) +### 1. Inline feedback (contextual) -Contextual to the field or action. Persistent until the state changes. +Feedback next to the field or action that triggered it. Persistent until state changes. -- **Success:** green tick/label next to the saved field or action area. E.g. "Saved" next to a settings field after change. -- **Error:** red border on the field + error text below it. Stays visible until fixed. Bold, clearly coloured, impossible to miss — same visual weight as validation errors. -- **Loading/progress:** subtle indicator while an async action is in-flight (e.g. "Syncing..." next to sync button). +- **Success:** green tick + "Saved" label next to save button. Auto-clears after 3s (LiveView only). +- **Error:** red border on field + error text below. Stays until fixed. Works identically with and without JS (changeset errors re-render the form). +- **Loading:** subtle spinner + "Saving..." / "Syncing..." while an async action is in-flight. Best for: form saves, field validation, toggle changes, individual actions. -### 2. Top banner (page-level confirmations) +### 2. Top banner (page-level) -For actions that affect the whole page or aren't tied to a specific field. +For outcomes not tied to a specific field. -- Bold, clearly coloured background: green for success, red for errors, amber for warnings -- Non-overlapping: pushes content down (not an overlay) -- **Persistent until manually dismissed** (click to close) — no auto-dismiss, because users may be looking elsewhere -- Clear close button (x icon) -- Positioned at top of the content area, below the header/nav +- Full-width coloured bar: green (success), red (error), amber (warning) +- **Document flow** — pushes content down, not an overlay +- **Persistent until dismissed** (close button). No auto-dismiss. +- Positioned at top of content area, below header/nav -Best for: "Products synced successfully", "Page saved", "Email provider connected", navigation-result confirmations, operation outcomes. +Best for: "Products synced", "Page deleted", auth messages, operation outcomes. ### 3. No floating toasts -Remove the floating toast pattern entirely. Between inline feedback and top banners, every use case is covered with better UX. +The fixed-position overlay toast pattern is removed entirely. -## Migration plan +## Progressive enhancement + +| Scenario | No JS (controller POST → redirect) | With JS (LiveView) | +|----------|-------------------------------------|---------------------| +| Save success | Banner via flash (PRG redirect) | Inline "Saved" tick next to button | +| Validation error | Inline field errors (controller re-renders form with changeset) | Inline field errors (LiveView validates on change/submit) | +| Page-level outcome | Banner via flash | Banner via flash | +| Connection lost | N/A (no WebSocket) | Banner via `phx-disconnected` | + +Banners are the baseline that works everywhere. Inline feedback is a progressive enhancement on top — LiveView pages get richer feedback, but no-JS pages still get inline validation errors via changeset re-rendering and success banners via flash. + +## Component spec + +### `<.banner>` — replaces `<.flash>` + +Reuses the existing flash infrastructure. Migration path: change CSS + move position in layout. Zero LiveView code changes needed — all 144 `put_flash` calls continue working. + +**Layout change** (admin): +```heex +<%!-- Move from AFTER the layout div (current line 228) to INSIDE .admin-layout-content, above
--%> +
+
...
+ <.flash_group flash={@flash} /> <%!-- NEW POSITION --%> +
...
+
+``` + +**Component change** (`core_components.ex`): +```heex +<%!-- Change wrapper class from .admin-toast (fixed position) to .admin-banner (document flow) --%> +
+
+

{msg}

+
+
+``` + +**CSS** (evolves from existing `.admin-banner-warning`): +```css +/* replaces .admin-toast (fixed position) */ +.admin-banner { + /* document flow, no position: fixed */ +} + +.admin-banner-inner { + display: flex; + align-items: flex-start; + gap: 0.75rem; + padding: 0.75rem 1rem; + font-size: 0.875rem; + line-height: 1.5; +} + +.admin-banner-info { + background: color-mix(in oklch, var(--t-status-info) 12%, var(--t-surface-base)); + color: var(--t-status-info); + border-bottom: 1px solid color-mix(in oklch, var(--t-status-info) 25%, var(--t-surface-base)); +} + +.admin-banner-error { + background: color-mix(in oklch, var(--t-status-error) 12%, var(--t-surface-base)); + color: var(--t-status-error); + border-bottom: 1px solid color-mix(in oklch, var(--t-status-error) 25%, var(--t-surface-base)); +} + +/* .admin-banner-warning already exists and matches this pattern */ +``` + +**No-JS**: Works perfectly. Banner is HTML in document flow — displays without JS. Close button uses `phx-click` so without LiveView the banner stays visible (desired — persistent by design). Flash clears naturally on next navigation. + +**Shop layout**: Same treatment — move `<.shop_flash_group>` from fixed overlay to document flow inside the content area. + +### `<.inline_feedback>` — new component + +Lightweight status indicator placed next to a button or form section. + +**Component** (`core_components.ex`): +```elixir +attr :status, :atom, values: [:idle, :saving, :saved, :error], default: :idle +attr :message, :string, default: nil + +def inline_feedback(assigns) do + ~H""" + + <.icon :if={@status == :saving} name="hero-arrow-path" class="size-4 motion-safe:animate-spin" /> + <.icon :if={@status == :saved} name="hero-check" class="size-4" /> + <.icon :if={@status == :error} name="hero-exclamation-circle" class="size-4" /> + {feedback_text(@status, @message)} + + """ +end + +defp feedback_text(:saving, _), do: "Saving..." +defp feedback_text(:saved, nil), do: "Saved" +defp feedback_text(:saved, msg), do: msg +defp feedback_text(:error, nil), do: "Something went wrong" +defp feedback_text(:error, msg), do: msg +defp feedback_text(:idle, _), do: nil +``` + +**CSS**: +```css +.admin-inline-feedback { + display: inline-flex; + align-items: center; + gap: 0.375rem; + font-size: 0.875rem; + line-height: 1.5; +} + +.admin-inline-feedback-saving { + color: var(--admin-text-soft); +} + +.admin-inline-feedback-saved { + color: var(--t-status-success, oklch(0.55 0.15 145)); +} + +.admin-inline-feedback-error { + color: var(--t-status-error); + font-weight: 600; +} +``` + +**Usage pattern** (LiveView): +```elixir +# Mount +assign(socket, save_status: :idle, save_error: nil) + +# Save handler +def handle_event("save", params, socket) do + case Context.save(params) do + {:ok, _} -> + Process.send_after(self(), :clear_save_status, 3000) + {:noreply, assign(socket, :save_status, :saved)} + {:error, reason} -> + {:noreply, assign(socket, save_status: :error, save_error: reason)} + end +end + +def handle_info(:clear_save_status, socket) do + {:noreply, assign(socket, :save_status, :idle)} +end +``` + +**Template**: +```heex +
+ <.button type="submit" variant="primary">Save + <.inline_feedback status={@save_status} message={@save_error} /> +
+``` + +### Connection error handling + +The `client-error` and `server-error` flashes (LiveView disconnect/reconnect) become banners too. Suppress them during intentional navigation to avoid a brief flash on refresh: + +**JS** (`app.js`): +```js +window.addEventListener("beforeunload", () => { + document.body.classList.add("navigating-away") +}) +``` + +**CSS**: +```css +.navigating-away #flash-group { display: none; } +``` + +`beforeunload` fires on refresh/navigation but NOT on genuine connection drops, so the banner still shows when actually needed. + +## Migration categories ### Category 1: Form saves → inline feedback -These should become contextual inline confirmations near the action: - -| Current flash | Location | Inline replacement | +| Current flash | Location | Replacement | |---|---|---| -| "Theme saved successfully" | Theme editor | Green tick next to save button, brief "Saved" label | -| "Email settings saved" | Email settings | Green tick/label near save area | -| "Settings saved" | Provider form | Green tick near save | -| "Navigation saved" | Navigation editor | Green tick near save | -| "Page settings saved" | Page editor | Green tick near save | -| "Metadata updated" | Media library | Green tick on the metadata form | -| "Product updated" | Product show | Green tick near the field that changed | +| "Theme saved successfully" | Theme editor | Inline "Saved" tick next to save button | +| "Email settings saved" | Email settings | Inline "Saved" tick near save | +| "Settings saved" | Provider form | Inline "Saved" tick near save | +| "Navigation saved" | Navigation editor | Inline "Saved" tick near save | +| "Page settings saved" | Page editor | Inline "Saved" tick near save | +| "Metadata updated" | Media library | Inline "Saved" tick on metadata form | +| "Product updated" | Product show | Inline "Saved" tick near changed field | ### Category 2: Validation errors → inline field errors -Already partially in place via changesets, but some are flash-based: +These flash-based validation messages become proper inline field errors. Works with and without JS — controller re-renders form with changeset errors, LiveView validates on change. -| Current flash | Location | Inline replacement | +| Current flash | Location | Replacement | |---|---|---| -| "Please enter your API token" | Setup wizard | Red border + error text on the API key field | +| "Please enter your API token" | Setup wizard | Red border + error on API key field | | "Missing required fields: {labels}" | Email settings | Red border on each missing field | -| "Please enter your Stripe secret key" | Settings | Red border + error on the Stripe key field | +| "Please enter your Stripe secret key" | Settings | Red border + error on Stripe key field | | "Please fill in all required fields" | Contact form | Red border on each empty required field | | "Please enter a valid email address" | Newsletter | Red border + error on email field | -| "Please enter your email address" | Various | Red border + error on email field | | "Password must be at least 12 characters" | Setup recover | Red border + error on password field | -| "Please enter a shop name" / "email address" | Setup | Red border on the relevant field | +| "Please enter a shop name" / "email" | Setup | Red border on relevant field | ### Category 3: State changes → top banner -These are page-level outcomes, best as persistent top banners: +Page-level outcomes, kept as `put_flash` → render as banner instead of toast: -| Current flash | Location | Banner replacement | -|---|---|---| -| "Shop is now live" / "Shop taken offline" | Settings | Green/amber banner at top of settings | -| "Connected to {provider}!" | Provider form | Green banner on the provider page | -| "Email provider disconnected" | Email settings | Amber banner | -| "Stripe connected and webhook endpoint created" | Settings | Green banner | -| "Provider connection deleted" | Providers index | Green banner | -| "Page saved" | Page editor | Green banner | -| "Page deleted" / "Page created" | Pages index | Green banner | +| Current flash | Location | +|---|---| +| "Shop is now live" / "Shop taken offline" | Settings | +| "Connected to {provider}!" | Provider form | +| "Email provider disconnected" | Email settings | +| "Stripe connected and webhook endpoint created" | Settings | +| "Provider connection deleted" | Providers index | +| "Page deleted" / "Page created" | Pages index | ### Category 4: Operation outcomes → top banner -Async or significant operations: +| Current flash | Location | +|---|---| +| "Sync started for {name}" | Providers/settings | +| "Connected! Product sync started in the background." | Setup | +| "Submission retry enqueued" | Activity log | +| "Image uploaded" / "Image deleted" | Media library | +| "Campaign is being sent!" | Newsletter | +| "Test email sent to {email}" | Email settings | +| "Message sent! We'll get back to you soon." | Contact | -| Current flash | Location | Banner replacement | -|---|---|---| -| "Sync started for {name}" | Providers/settings | Green banner with progress note | -| "Connected! Product sync started in the background." | Setup | Green banner | -| "Submission retry enqueued" | Activity log | Green banner | -| "Image uploaded" / "Image deleted" | Media library | Green banner | -| "Campaign is being sent!" | Newsletter | Green banner | -| "Test email sent to {email}" | Email settings | Green banner | -| "Added to basket" | Shop cart | Inline: brief confirmation near the add button or cart icon update | -| "Removed from basket" | Shop cart | Inline: item visually removed, no separate message needed | -| "Message sent! We'll get back to you soon." | Contact | Green banner above the form (or replace form with success state) | +### Category 5: Auth/system → top banner -### Category 5: Auth/system messages → top banner (keep as-is pattern) +| Current flash | Location | +|---|---| +| "Welcome back!" | Dashboard | +| "Logged out successfully" | Login page | +| "Invalid email or password" | Login (could be inline under form) | +| "User confirmed successfully" | Email confirm | -These are inherently page-level and work fine as top banners: - -| Current flash | Location | Notes | -|---|---|---| -| "Welcome back!" | Login redirect | Green banner on dashboard | -| "Logged out successfully" | Post-logout | Green banner on login page | -| "Invalid email or password" | Login | Could be inline under the form, or red banner | -| "User confirmed successfully" | Email confirm | Green banner | -| "Your basket is empty" | Checkout | Redirect + banner or inline empty state | - -### Category 6: Error/failure messages → inline or banner +### Category 6: Cart feedback → inline | Current flash | Replacement | |---|---| -| "Could not connect — check your API key" | Inline error on the API key field + help link | -| "Stripe connection failed: {message}" | Inline error near Stripe key field | -| "Failed to send test email: {reason}" | Inline error near the test email button | -| "Upload failed" | Inline error near the upload area | -| "Cannot delete — image is still in use" | Red banner with explanation | -| "Something went wrong. Please try again." | Red banner | +| "Added to basket" | Brief inline confirmation near add button / cart icon badge update | +| "Removed from basket" | Item visually removed, no separate message | -## Implementation +## Implementation plan -### Phase 1: Build the notification components +### Phase 1: Build components (~2h) -1. **Inline feedback component** — reusable component that shows success/error state next to any action. Props: status (success/error/loading), message, auto-clear timing (optional, for success only after state persists). -2. **Top banner component** — replaces the current flash component. Pushes content down, persistent until dismissed, coloured by type. -3. **Remove** the existing floating toast CSS/JS. +Build both components. Convert flash rendering from toast to banner. No behaviour changes — all existing `put_flash` calls work, just render differently. -### Phase 2: Migrate admin forms (highest impact) +1. Restyle `<.flash>` from `.admin-toast` (fixed) to `.admin-banner` (document flow) +2. Move `<.flash_group>` in admin layout from after the div to inside content area +3. Same for shop layout — restyle `.shop-flash` to document flow +4. Build `<.inline_feedback>` component in `core_components.ex` +5. Add `beforeunload` suppression for connection error banners +6. Delete old `.admin-toast` and `.shop-flash-group` (fixed-position) CSS -Start with the most-used admin pages: +### Phase 2: Migrate admin forms (~3h) + +Replace `put_flash(:info, "Saved")` with inline feedback on the highest-traffic pages: - Theme editor - Page editor - Settings - Email settings - Provider forms -### Phase 3: Migrate remaining admin pages +Each migration: add `:save_status` assign, swap `put_flash` for `assign(:save_status, :saved)`, add `<.inline_feedback>` to template, add `Process.send_after` auto-clear. + +### Phase 3: Migrate remaining admin pages (~2h) - Media library - Products @@ -146,27 +325,25 @@ Start with the most-used admin pages: - Redirects - Navigation -### Phase 4: Migrate shop pages +### Phase 4: Migrate shop pages (~2h) -- Cart (add/remove feedback) -- Contact form +- Cart (add/remove → inline feedback) +- Contact form (success → replace form with success state) - Checkout errors - Auth flows (login, registration, confirmation) -### Phase 5: Migrate setup wizard +### Phase 5: Migrate setup wizard (~1h) -Update the setup flow (ties into the onboarding-ux v2 plan). +Update setup flow — ties into onboarding-ux v2 plan. If onboarding tasks A/B are done first, this phase applies inline feedback to the new guided flow. ## Task breakdown | # | Task | Est | Status | |---|------|-----|--------| -| 1 | Build inline feedback component | 1.5h | planned | -| 2 | Build persistent top banner component (replaces flash) | 1.5h | planned | -| 3 | Migrate admin forms to inline feedback (theme, pages, settings, email, providers) | 3h | planned | -| 4 | Migrate remaining admin pages (media, products, activity, newsletter, redirects, nav) | 2h | planned | -| 5 | Migrate shop pages (cart, contact, checkout, auth) | 2h | planned | -| 6 | Migrate setup wizard notifications | 1h | planned | -| 7 | Remove old flash/toast CSS and JS | 30m | planned | +| 1 | Build banner + inline feedback components, restyle flash rendering | 2h | planned | +| 2 | Migrate admin forms (theme, pages, settings, email, providers) | 3h | planned | +| 3 | Migrate remaining admin pages (media, products, activity, newsletter, redirects, nav) | 2h | planned | +| 4 | Migrate shop pages (cart, contact, checkout, auth) | 2h | planned | +| 5 | Migrate setup wizard notifications | 1h | planned | -Total estimate: ~11.5h +Total estimate: ~10h