docs/js-hooks.md

# JS Hooks

PureAdmin ships 14 JavaScript hooks for interactive features. Import them all via `PureAdminHooks` or individually.

## Setup

```javascript
import { PureAdminHooks } from "keen_pure_admin"

let liveSocket = new LiveSocket("/live", Socket, {
  hooks: { ...PureAdminHooks }
})
```

## Available Hooks

### PureAdminSettings

Settings panel for theme mode, layout width, sidebar, fonts. Fetches theme manifests from `/api/themes/manifests` and dynamically populates the theme selector. All settings persist to `localStorage`.

Used by: `<.settings_panel />`

### PureAdminProfilePanel

Profile panel with tab switching, favorites management, and click-outside-to-close.

Used by: `<.profile_panel />`

### PureAdminTooltip

CSS-only tooltip positioning using Floating UI. Handles placement, auto-flip, and theme color variants.

Used by: `<.tooltip />` (when using floating/JS positioning)

### PureAdminPopover

Click-triggered popover with title, placement, size, and alignment. Uses Floating UI for positioning, moves content to `document.body` to avoid clipping.

Used by: `<.popover />`

### PureAdminToast

Toast auto-dismiss with configurable duration. Handles show/hide transitions and progress bar animation. Renders toasts client-side from `push_event` data.

Used by: `<.toast_container is_hook />`

### PureAdminFlash

Independent inline flash message containers. Multiple containers on the same page receive messages independently via `push_flash/5`. Renders `pa-alert` elements client-side. Supports markdown body (**bold**, *italic*, `[links](url)`, lists, `---` horizontal rules), action buttons with server callbacks, and auto-dismiss.

Used by: `<.flash_container />`

**Usage:**

```heex
<.flash_container id="my-form" />
```

```elixir
# Simple flash
socket |> push_flash("my-form", "success", "Saved!")

# With markdown body and action buttons
socket |> push_flash("my-form", "warning", """
Are you sure you want to delete **Invoice #1234**?

This action cannot be undone.
""",
  title: "Confirm Deletion",
  actions: [
    %{label: "Delete", event: "delete-record", params: %{id: 1234}, variant: "danger"},
    %{label: "Cancel", dismiss: true, variant: "secondary"}
  ])
```

**Options:**

| Option | Default | Description |
|---|---|---|
| `:title` | `nil` | Heading above the message |
| `:duration` | `0` | Auto-dismiss in ms (0 = persistent) |
| `:dismissible` | `true` | Show close button |
| `:actions` | `[]` | Action button maps (see above) |

### PureAdminCommandPalette

Spotlight-style command palette with three modes:

- **Commands** (`/prefix`) — multi-step action wizards with step progression
- **Search contexts** (`:prefix`) — scoped entity search
- **Global search** (no prefix) — search across everything

Keyboard: Ctrl+K toggle, ↑↓ navigate, Enter/Tab select, Escape/Backspace-at-0 step back. Debounced search input (150ms) for search modes, instant for command/context list filtering.

Used by: `<.command_palette />`

**Event protocol (hook → LiveView):**

| Event | Payload | When |
|---|---|---|
| `cp:toggle` | `{}` | Ctrl+K |
| `cp:close` | `{}` | Escape at top level, backdrop click |
| `cp:input` | `{query}` | Input changed |
| `cp:navigate` | `{direction}` | Arrow up/down |
| `cp:page` | `{direction}` | Arrow left/right (search modes) |
| `cp:select` | `{index}` | Enter, Tab, or click |
| `cp:step_back` | `{}` | Backspace at pos 0, Escape in step/context mode |

**LiveView → hook (push_event):**

| Event | Payload | Purpose |
|---|---|---|
| `cp:focus` | `{}` | Focus input |
| `cp:reset_input` | `{value}` | Force-set input value on mode transition |

**Registering commands:**

```elixir
commands = [
  %{
    id: "deploy",
    shortcut: "/deploy",
    aliases: ["/d"],
    name: "Deploy to Environment",
    description: "Deploy a branch to an environment",
    icon: "🚀",
    steps: [
      %{id: "environment", prompt: " in ", placeholder: "Select environment..."},
      %{id: "branch", prompt: " branch ", placeholder: "Type branch...", free_text: true}
    ]
  }
]
```

**Registering search contexts:**

```elixir
contexts = [
  %{id: "products", shortcut: ":products", aliases: [":p"], name: "Products", icon: "📦"}
]
```

**Handling command completion:**

```elixir
def handle_info({:command_complete, "deploy", selections}, socket) do
  env = Enum.find(selections, & &1.step_id == "environment")
  # Do something with selections...
  {:noreply, socket}
end
```

**Display styles:**

Two visual modes for command step progression:

- `display="inline"` (default) — Svelte-style. Input shows the full accumulated sentence (e.g., `/assign iPad Air to`). A command badge appears on the right. The locked prefix can't be deleted.
- `display="tokens"` — Token-style. Previous selections render as colored spans above a clean input. Each step starts with an empty input.

```heex
<.command_palette display="inline" ... />
<.command_palette display="tokens" ... />
```

### PureAdminDetailPanel

Detail panel toggle for inline split-view and overlay modes.

Used by: Detail panel patterns (see detail panel demo)

### PureAdminSidebarResize

Drag-to-resize sidebar with mouse/touch events. Stores width in `localStorage`.

Used by: `<.sidebar />` with `is_resizable` setting

### PureAdminCharCounter

Character counter for textarea/input fields. Configurable max length with translatable message templates via `data-msg`/`data-msg-over` with `{count}`/`{max}` placeholders.

Used by: `<.input type="textarea" />` with `maxlength` and char counter

### PureAdminCheckbox

Syncs the `indeterminate` property from `data-indeterminate` attribute. Required for tri-state checkboxes since HTML doesn't have an `indeterminate` attribute.

Used by: `<.checkbox is_indeterminate />`

### PureAdminSplitButton

Split button dropdown via Floating UI. Manages open/close state, closes other open split buttons, and handles `pushEvent` for menu item clicks and inline action buttons. Since the menu is moved to `document.body` for positioning, native `phx-click` doesn't work — the hook forwards clicks via `pushEvent`.

Used by: `<.split_button />`

### PureAdminSidebarSubmenu

Persists sidebar submenu open/closed state to `localStorage`. Restores state on mount, URL-active submenus always win over stored state. Uses `MutationObserver` to detect JS command class changes.

Used by: `<.sidebar_submenu />`

### PureAdminInfiniteScroll

IntersectionObserver-based infinite scroll. Fires a LiveView event when a sentinel element scrolls into view. Configurable throttle and preload buffer.

**Data attributes:**

| Attribute | Default | Description |
|---|---|---|
| `data-event` | `"load_more"` | LiveView event to push |
| `data-has-more` | `"true"` | Set to `"false"` to stop |
| `data-throttle` | `"500"` | Min ms between triggers |
| `data-root-margin` | `"200px"` | Preload buffer distance |

**Usage:**

```heex
<div
  id="scroll-sentinel"
  phx-hook="PureAdminInfiniteScroll"
  data-event="load_more"
  data-has-more={to_string(@has_more)}
>
  <.loader :if={@loading} />
</div>
```

## Page Context

Server-rendered JSON available to JS synchronously via a hidden input. Avoids API fetches on page load.

```javascript
import { getPageContext, getContextValue } from "keen_pure_admin"

const ctx = getPageContext()                    // full context
const manifests = getContextValue("themeManifests")  // single key
```

The settings panel reads `themeManifests` from the context automatically. Apps register providers:

```elixir
config :keen_pure_admin,
  page_context_providers: [&MyApp.PageContext.theme_manifests/1]
```

Render in root layout: `<.page_context />`

## Logging

Categorized, color-coded, silent by default. Enable at runtime:

```javascript
// Browser console
PureAdmin.logging.enableLogging()          // all → debug
PureAdmin.logging.setCategoryLevel('PA:SETTINGS', 'debug')  // one category
PureAdmin.logging.getCategories()          // list all

// KeenMate convention
window.components['keen-pure-admin'].logging.enableLogging()
```

Categories: `PA:SETTINGS`, `PA:CMD_PALETTE` (more added as hooks are instrumented).

Use in custom hooks:

```javascript
import { createLogger } from "keen_pure_admin"
const log = createLogger('MY_HOOK')
log.debug('mounted')
```

## Modal Dialogs (non-hook)

Programmatic dialogs are initialized separately:

```javascript
import { initModalDialogs } from "keen_pure_admin"

initModalDialogs()
```

This enables `PureAdmin.confirm()`, `PureAdmin.alert()`, and `PureAdmin.prompt()` globally.