# App Loading Spinner — Design & How To Use (2026-09-26)

One loading spinner for the whole app: a **ring of 8 dots with a fading tail that turns in 8 steps** (the iOS/"activity indicator" look), coloured blue (`#3b82f6`) with grey `loading` text underneath. It replaces the old rotating-arc spinners everywhere.

---

## 1. The look (source of truth)

| Property | Value |
|---|---|
| Shape | 8 dots on a circle (viewBox `0 0 60 60`, ring radius 22, dot radius 4) |
| Fading tail | dot opacities `1, .8, .62, .48, .36, .27, .2, .15` (bright dot leads, tail fades behind) |
| Motion | `animation: cy-dots .5s steps(8) infinite` — one full turn every 0.5s, moving in 8 hard steps (not a smooth spin) |
| Colour | blue `#3b82f6` for full-page/overlay spinners. Small in-button spinners take the button's text colour (`currentColor`) |
| Text | the word **loading** (lowercase), `.8rem`, font-weight 400, letter-spacing 0, grey `#6b7280` |
| Overlay | white at 75% opacity, covers the **content area only** (starts where the sidebar ends), sidebar and top bar stay on top |

Keyframes (global, defined once in `AdminPanelProvider`): `@keyframes cy-dots { to { transform: rotate(360deg); } }`

---

## 2. Three layers — what covers what

### Layer 1 — global CSS (covers every existing spinner, no page edits)
File: `app/Providers/Filament/AdminPanelProvider.php`, in the panel `<style>` block (search `App-wide loading spinner`).

Any spinner-type `<svg>` is turned into the dots ring with a CSS **mask**: its own drawing (`> *`) is hidden, the element is filled with `currentColor` and masked by the 8-dot image (`--cy-dots`), and animated with `cy-dots`. Selectors covered:

| Selector | What it catches |
|---|---|
| `svg.fi-loading-indicator` | Filament's own spinner: buttons submitting, tables loading, modals, `generate_loading_indicator_html()` (used everywhere in Filament) |
| `svg.animate-spin` | Tailwind spinners (`channel-activity-log`, etc.) |
| `svg[style*="animation:spin"]`, `svg[style*="animation: spin"]` | the inline `animation:spin 1s linear infinite` SVGs used on most plugin pages (sku-mappings, product-import, order-sync, allmakes pages, …) |
| `svg[style*="-spin "]`, `svg[style*="cspin"]` | page-specific keyframe names (`cm-spin`, `cspin`, …) |
| `svg.po-spinner`, `svg.eos-spin`, `svg.gr-spin`, `svg.cle-spin`, `svg[class*="-spin"]` | class-based spinners (po-create, ebay-order-sync, shopify-gap-report, listing-modal, …) |

Because the dots use `currentColor`, an existing spinner keeps whatever colour its page gave it (e.g. `color:#6366f1`).

### Layer 2 — reusable Blade component (use this for anything NEW)
File: `plugins/webkul/support/resources/views/components/loading-spinner.blade.php`

```blade
<x-support::loading-spinner />                              {{-- 40px, blue, "loading" underneath --}}
<x-support::loading-spinner :size="24" label="" />          {{-- no text --}}
<x-support::loading-spinner :size="44" color="#6366f1" />   {{-- custom colour --}}
```

Props: `size` (px, default 40), `label` (default `loading`, empty = no text), `color` (default `#3b82f6`). It also accepts normal attributes (`class`, `style`, `x-show`, `wire:loading`).

From PHP (e.g. inside an `HtmlString`): `\Illuminate\Support\Facades\Blade::render('<x-support::loading-spinner :size="44" />')`.

### Layer 3 — the content-area overlay pattern
Used by the PO status dropdown (`purchases/.../PurchaseOrderResource/Pages/Concerns/HasInlineStatusDropdown.php`). Copy it when a whole page should dim while something loads.

```html
<div x-data="{ busy: false, left: 0, start() {
        const main = document.querySelector('.fi-main-ctn');
        this.left = main ? main.getBoundingClientRect().left : 0;   // where the sidebar ends
        this.busy = true; setTimeout(() => this.busy = false, 30000); // safety reset if the request fails
     } }">
    <select x-on:change="start()" x-bind:disabled="busy" ...>...</select>

    {{-- OUTER element: only x-show + position. INNER element: the centring. --}}
    <div x-show="busy" x-cloak x-bind:style="{ left: left + 'px' }"
         style="position:fixed;top:0;right:0;bottom:0;z-index:10;background:rgba(255,255,255,.75);">
        <div style="position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;">
            <x-support::loading-spinner :size="44" />
        </div>
    </div>
</div>
```

---

## 3. Rules / gotchas learned the hard way

1. **`x-show` removes the inline `display` value** of the element it controls. If that same element had `display:flex` for centring, the centring is lost (the spinner jumped to the top-left corner). Put `x-show` on an outer element and the flex centring on an inner one.
2. **z-index:** overlay = `10`, which is above ordinary cards but **below the sidebar (`z-20`) and top bar**, so the menu stays visible and un-dimmed. Never use 9999 for a page overlay.
3. **Position from `.fi-main-ctn`** (`getBoundingClientRect().left`) so the overlay starts where the sidebar ends. It adapts if the sidebar is collapsed because it is measured when the overlay opens.
4. **Always add a safety reset** (the 30s timeout) for overlays started from Alpine, otherwise a failed request leaves the page dimmed.
5. **Mask needs `currentColor`**: set the spinner's colour with `color:` (text colour), not `stroke`/`fill`.
6. The `[class*="-spin"]` selector is deliberately broad; if a non-spinner SVG ever gets caught (a class name containing `-spin`), rename its class or add `svg.that-class { mask: none !important; ... }`.

---

## 4. What is NOT converted (yet)

Anything that is **not an `<svg>`**, e.g. `div` spinners drawn with CSS borders:
- `.np-spinner` in `allmakes-psp/.../allmakes-new-product.blade.php` (a bordered circle div)
- any `border-top-color` + `border-radius:50%` + `animation` div found later (`grep -rn "border-radius:50%" | grep animation`)

Convert those by replacing the div with `<x-support::loading-spinner :size="18" label="" />`.

Also not touched: text-only loading states ("Loading…" with no spinner) and the Filament page progress bar at the very top of the screen (thin blue line, separate from spinners).

---

## 5. Where this was applied in this session

- Global CSS + component created (layers 1 and 2).
- PO status dropdown overlay (layer 3) now uses the shared component instead of its own inline SVG.
- Tests: `tests/Feature/LoadingSpinnerComponentTest.php` (component renders 8 dots, optional label; CSS selectors are present) and `PurchaseOrderViewPageTest.php`.

## 6. How to check it

1. Hard refresh (Ctrl+F5) so the new CSS loads.
2. Trigger any loading state: change a PO status; click a button that submits a form (Filament button spinner); on Channels → Listings click a sync/push button; open Product Import.
3. All should show the dots ring. If one still shows the old arc, inspect the `<svg>` and note its class / inline `animation:` name, then add it to the selector list in section 2 (Layer 1).
