# Tooltip System — How It Works

## The Problem

Filament v4 uses two completely different tooltip mechanisms depending on context. Getting consistent styling across all of them was non-trivial.

---

## Two Types of Tooltips in Filament

### 1. Tippy.js tooltips
Used by: custom column HTML, some Filament action buttons that explicitly call `->tooltip()`

These are created via the Alpine.js `x-tooltip` directive:
```html
<button x-tooltip="{
    content: 'Filter',
    theme: $store.theme,
}">
```
Tippy appends a `[data-tippy-root]` div to `<body>` containing:
```html
<div data-tippy-root>
  <div class="tippy-box" data-theme="light">
    <div class="tippy-content">Filter</div>
  </div>
</div>
```

### 2. Browser native `title` attribute tooltips
Used by: **Filter button**, **Column Manager button**, and any `<x-filament::icon-button>` that does NOT have an explicit `->tooltip()` set.

In `vendor/filament/support/resources/views/components/icon-button.blade.php`:
```php
'title' => $hasTooltip ? null : $label,
```
When no tooltip prop is passed, the component falls back to a plain HTML `title` attribute. This renders the OS/browser native tooltip — completely unstyled and impossible to target with CSS.

---

## Why CSS Overrides Alone Failed

Filament's tippy theme CSS (for `data-theme="light"` and `data-theme="dark"`) is **bundled by Vite and injected into `<head>` via JavaScript at runtime**, after the initial HTML is parsed. This means even a `<style>` tag with `!important` in `STYLES_AFTER` can lose if Filament's JS-injected stylesheet is appended to `<head>` after ours.

---

## The Solution (three layers)

All code lives in `app/Providers/Filament/AdminPanelProvider.php` across two render hooks:
- `PanelsRenderHook::STYLES_AFTER` — static CSS (first-pass attempt)
- `PanelsRenderHook::SCRIPTS_AFTER` — JavaScript fixes

---

### Layer 1 — CSS pinned to end of `<head>` via JS

A `<style id="cy-tippy-css">` element is created and injected by JavaScript. A `MutationObserver` watches `<head>` and **moves it to the very end** whenever anything new is appended (e.g. Filament/Vite injecting their theme CSS). Last stylesheet wins in the CSS cascade.

```javascript
function pinTippyCss() {
    var el = document.getElementById('cy-tippy-css');
    if (!el) {
        el = document.createElement('style');
        el.id = 'cy-tippy-css';
        el.textContent = TIPPY_CSS; // our override rules
    }
    if (el !== document.head.lastElementChild) {
        document.head.appendChild(el); // move to end
    }
}
new MutationObserver(function () { pinTippyCss(); })
    .observe(document.head, { childList: true });
```

---

### Layer 2 — Inline styles via MutationObserver (tippy boxes)

Even if CSS loses, inline styles with `!important` always win. A `MutationObserver` watches `<body>` for:
- New child nodes being added (tippy appending its root div)
- `data-theme` or `data-state` attribute changes on `.tippy-box` elements

A `setTimeout(0)` defers execution so tippy can finish setting `data-theme` before we override it. A `mouseover` capture listener also fires `styleAllTippyBoxes()` as a final safety net.

---

### Layer 3 — `title` attribute interception (Filter + Column Manager)

Browser native `title` tooltips cannot be styled. The fix:
1. Scan the DOM for all `[title]` elements
2. Remove the `title` attribute (stops the native tooltip)
3. Store the text as `data-cy-title`
4. A capture-phase `mouseover` listener shows a fixed-position custom tooltip

A `MutationObserver` on `<body>` re-runs the scan on every Livewire partial update so dynamically rendered elements are also caught.

```javascript
function convertTitles() {
    document.querySelectorAll('[title]').forEach(function (el) {
        if (el._cyTitleDone) return;
        var t = el.getAttribute('title');
        if (!t) return;
        el._cyTitleDone = true;
        el.removeAttribute('title');
        el.setAttribute('data-cy-title', t);
    });
}
```

The floating tooltip element (`#cy-global-tt`) is a single `div` appended to `<body>` with `position: fixed`. Its `left` position is clamped to `window.innerWidth - tooltipWidth - 8` so it never overflows the right edge of the screen (which was an issue for the Column Manager button).

---

### Layer 4 — Custom `.cy-tt` column tooltips

Table columns that use `->html()` and `getStateUsing()` return raw HTML. Filament's `Str::sanitizeHtml()` strips `data-*` attributes but preserves `class` and `style`. So tooltips in custom columns use a wrapper structure:

```html
<span class="cy-tt">
  Badge content
  <span class="cy-tt-label">Tooltip text</span>
</span>
```

The `.cy-tt-label` is hidden by default (`opacity: 0; position: fixed`). The same capture-phase `mouseover` listener positions it using `getBoundingClientRect()` and sets `opacity: 1`.

This escapes Filament table's `overflow: hidden` — `position: absolute` was clipped; `position: fixed` is not.

---

## Tooltip Styles (shared)

All tooltips (tippy, title-converted, and cy-tt) use the same visual style:

| Property | Value |
|----------|-------|
| Background | `#ffffff` |
| Text colour | `#1e293b` |
| Border | `1px solid #e2e8f0` |
| Border radius | `6px` |
| Box shadow | `0 4px 12px rgba(0,0,0,0.18)` |
| Font size | `0.72rem` / `0.75rem` |

---

## Files Changed

| File | What changed |
|------|-------------|
| `app/Providers/Filament/AdminPanelProvider.php` | All tooltip JS + CSS overrides in `STYLES_AFTER` and `SCRIPTS_AFTER` render hooks |

---

## If Tooltips Break in Future

1. **tippy tooltips look wrong** → Check if Filament updated its CSS bundle; the `pinTippyCss()` head observer should still pin our overrides last, but verify the `TIPPY_CSS` string in `SCRIPTS_AFTER` still targets the right selectors (`[data-theme~="light"]`, `[data-theme~="dark"]`).

2. **Filter / Column Manager tooltip disappears** → Filament may have added an explicit `->tooltip()` to those actions, meaning they now use tippy (good). Remove any workaround if needed.

3. **Column tooltip clipped** → Check that `.cy-tt-label` still has `position: fixed` in the CSS. If Filament updates its table CSS to not clip overflow, `position: absolute` would be cleaner.

4. **Tooltip off right edge** → The `maxLeft` clamp in `showFloater()` uses `f.offsetWidth` — if the floater is not yet in the DOM when first shown, `offsetWidth` returns 0 and the clamp won't fire. The floater is created on first call to `getFloater()` and appended immediately, so this should not be an issue in practice.
