README.md

# PureAdmin

[![Hex.pm](https://img.shields.io/hexpm/v/keen_pure_admin.svg)](https://hex.pm/packages/keen_pure_admin)
[![Hex Docs](https://img.shields.io/badge/hex-docs-blue.svg)](https://hexdocs.pm/keen_pure_admin)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)

Phoenix LiveView component library wrapping the [Pure Admin](https://github.com/KeenMate/pure-admin) CSS framework into function components and LiveComponents.

Drop-in replacement for Phoenix `CoreComponents` -- provides `button/1`, `badge/1`, `card/1`, `modal/1`, `table/1`, `input/1`, and 35+ more components with full BEM class support.

**Main site:** [pureadmin.io](https://pureadmin.io) -- themes, documentation, and component showcase

**Live demo:** [elixir.demo.pureadmin.io](https://elixir.demo.pureadmin.io)

## Prerequisites

Create a new Phoenix project **without Tailwind** — Pure Admin provides its own CSS framework:

```bash
mix phx.new my_app --no-tailwind
```

> If you have an existing project that uses Tailwind, remove the Tailwind dependency and its configuration before adding Pure Admin, as the two CSS frameworks will conflict.

## Installation

Add `keen_pure_admin` to your list of dependencies in `mix.exs`:

### From Hex (recommended)

```elixir
def deps do
  [
    {:keen_pure_admin, "~> 1.0.0-rc.1"}
  ]
end
```

### From GitHub

```elixir
def deps do
  [
    {:keen_pure_admin, github: "KeenMate/keen-pure-admin", tag: "v1.0.0-rc.1"}
  ]
end
```

### Local path (for development)

```elixir
def deps do
  [
    {:keen_pure_admin, path: "../keen-pure-admin"}
  ]
end
```

Then fetch dependencies:

```bash
mix deps.get
```

## Setup

### 1. Import components

Replace your `CoreComponents` import with `PureAdmin.Components`:

```elixir
# In your app's html_helpers or MyAppWeb module
use PureAdmin.Components
```

This replaces `button/1`, `input/1`, `simple_form/1`, `modal/1`, `table/1`, `list/1`, `label/1`, `flash/1`, and `flash_group/1`. A few CoreComponents functions are not replaced:

- **`header/1`** — use `@page_title` in `<.navbar_title>` (the layout renders it, each LiveView sets it)
- **`icon/1`** — use Font Awesome directly: `<i class="fa-solid fa-user"></i>`
- **`translate_error/1`** — keep your app's Gettext-based implementation or copy it from the generated CoreComponents
- **`show/1`**, **`hide/1`** — use `Phoenix.LiveView.JS.show/1` and `JS.hide/1` directly

### 2. Include Pure Admin CSS

This library generates HTML with BEM classes matching [`@keenmate/pure-admin-core`](https://www.npmjs.com/package/@keenmate/pure-admin-core). You need to include the Pure Admin CSS in your project.

Install the CSS framework via npm:

```bash
cd assets
npm install @keenmate/pure-admin-core
```

Then import it in your CSS:

```css
@import "@keenmate/pure-admin-core";
```

Or use a CDN in your `root.html.heex`:

```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@keenmate/pure-admin-core/dist/pure-admin.min.css" />
```

### 3. Register JS hooks

```javascript
// assets/js/app.js
import { PureAdminHooks } from "keen_pure_admin"

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

### 4. Add Floating UI (required for tooltips, popovers, split buttons)

```html
<script src="https://cdn.jsdelivr.net/npm/@floating-ui/core@1.6.9"></script>
<script src="https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.6.13"></script>
```

Or install via npm:

```bash
cd assets
npm install @floating-ui/dom
```

### 5. Add Font Awesome (icons)

```html
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
```

### 6. Add FOUC prevention (optional)

In your root layout, add the script before `{@inner_content}` to prevent flash of unstyled content when using the settings panel or sidebar submenus:

```heex
<body>
  <.fouc_prevention_script />
  {@inner_content}
</body>
```

### 7. Add global toast service (optional)

Add a toast container to your app layout for app-wide toast notifications:

```heex
<.toast_container id="toasts" position="top-end" is_hook />
```

Then push toasts from any LiveView:

```elixir
alias PureAdmin.Components.Toast, as: PureToast

socket |> PureToast.push_toast("success", "Saved!", "Changes saved successfully.")
```

For cross-page delivery (e.g., background tasks that complete after navigation), broadcast via PubSub and handle in an `on_mount` hook:

```elixir
# In your on_mount hook
Phoenix.PubSub.subscribe(MyApp.PubSub, "toasts")

attach_hook(socket, :global_toasts, :handle_info, fn
  {:push_toast, variant, title, message, opts}, socket ->
    {:halt, PureToast.push_toast(socket, variant, title, message, opts)}
  _other, socket ->
    {:cont, socket}
end)

# From a background task
Phoenix.PubSub.broadcast(MyApp.PubSub, "toasts",
  {:push_toast, "success", "Done!", "Task completed.", duration: 0})
```

## Components

### Layout

Full page structure matching the Pure Admin three-section navbar + sidebar + content + footer pattern:

```heex
<.layout>
  <.navbar>
    <:start>
      <.navbar_burger />
      <.navbar_brand><.heading level="1">My App</.heading></.navbar_brand>
      <.navbar_nav>
        <.navbar_nav_item href="/">Dashboard</.navbar_nav_item>
        <.navbar_nav_item href="/reports" has_dropdown>
          Reports
          <:dropdown>
            <.navbar_dropdown>
              <.navbar_nav_item href="/reports/sales">Sales</.navbar_nav_item>
              <.navbar_nav_item href="/reports/users">Users</.navbar_nav_item>
            </.navbar_dropdown>
          </:dropdown>
        </.navbar_nav_item>
      </.navbar_nav>
    </:start>
    <:center>
      <.navbar_title><.heading level="2">Dashboard</.heading></.navbar_title>
    </:center>
    <:end_>
      <.notifications count={3}>
        <.notification_item variant="primary" icon="fa-solid fa-bell" is_unread>
          <:title>New message</:title>
          <:text>You have a new message</:text>
          <:time>2 min ago</:time>
        </.notification_item>
      </.notifications>
      <.navbar_profile_btn name="John Doe" phx-click={toggle_profile_panel()} />
    </:end_>
  </.navbar>

  <.layout_inner>
    <.sidebar>
      <.sidebar_item label="Dashboard" icon="fa-solid fa-gauge" href="/" is_active />
      <.sidebar_submenu id="settings" label="Settings" icon="fa-solid fa-gear" is_open={String.starts_with?(@current_path, "/settings")}>
        <.sidebar_item label="General" href="/settings" />
        <.sidebar_item label="Security" href="/settings/security" />
      </.sidebar_submenu>
    </.sidebar>

    <.layout_content>
      <.main>
        <.flash_group flash={@flash} />
        {@inner_content}
      </.main>
      <.footer>
        <:start>&copy; 2026 My App</:start>
      </.footer>
    </.layout_content>
  </.layout_inner>
</.layout>
```

### Profile Panel

Slide-out profile panel with avatar, tabs, navigation, and click-outside-to-close:

```heex
<.profile_panel name="John Doe" email="john@example.com" role="Admin">
  <:tabs>
    <div class="pa-tabs pa-tabs--full">
      <button class="pa-tabs__item pa-tabs__item--active" data-profile-tab="profile">
        <i class="fa-solid fa-user"></i>
        <span class="pa-profile-panel__tab-text">Profile</span>
      </button>
      <button class="pa-tabs__item" data-profile-tab="favorites">
        <i class="fa-solid fa-star"></i>
        <span class="pa-profile-panel__tab-text">Favorites</span>
      </button>
    </div>
  </:tabs>

  <div class="pa-tabs__panel pa-tabs__panel--active" data-profile-panel="profile">
    <nav class="pa-profile-panel__nav">
      <ul>
        <.profile_nav_item href="/profile" icon="fa-solid fa-user">Settings</.profile_nav_item>
        <.profile_nav_item href="/logout" icon="fa-solid fa-right-from-bracket">Sign Out</.profile_nav_item>
      </ul>
    </nav>
  </div>

  <:footer_>
    <button class="pa-btn pa-btn--danger pa-btn--block">Sign Out</button>
  </:footer_>
</.profile_panel>
```

### Settings Panel

Client-side settings panel for theme mode, layout width, sidebar options, fonts, and more -- all persisted to localStorage:

```heex
<.settings_panel />
```

### UI Components

| Component | Description |
|---|---|
| `button/1`, `split_button/1`, `button_group/1` | Buttons with variants, sizes, loading, split dropdown, responsive groups |
| `badge/1`, `label/1`, `composite_badge/1`, `badge_group/1` | Badges, labels, composite badges with expand/collapse |
| `alert/1` | Dismissible alerts |
| `callout/1` | Callout/info boxes |
| `card/1` | Cards with header (title/subtitle/description), body, footer, tabs |
| `modal/1` | Modal dialogs |
| `popconfirm/1` | Popconfirm dialogs anchored to trigger buttons |
| `table/1`, `table_card/1`, `table_container/1` | Data tables with sorting, card wrappers, responsive grid |
| `comparison_table/1`, `comparison_row/1`, `comparison_value/1` | Two/three-column data comparison with change/conflict highlighting |
| `tabs/1` | Tab navigation with panels |
| `input/1`, `form_group/1`, `input_wrapper/1` | Form inputs with labels, errors, clear button |
| `filter_card/1` | Expandable filter card with advanced filters |
| `grid/1`, `column/1` | Flexbox grid with percentage/fraction columns |
| `section/1` | Content section with optional `title_text` heading |
| `stat/1` | Stat cards (hero, square) |
| `timeline/1` | Timeline displays |
| `loader/1`, `loader_center/1`, `loader_overlay/1` | Loading spinners |
| `basic_list/1`, `ordered_list/1`, `definition_list/1` | HTML lists with spacing, icons, borders |
| `checkbox_list/1`, `checkbox_list_item/1`, `checkbox_box/1` | Checkbox lists with variants, layouts, actions |
| `list/1`, `list_item/1` | Complex lists with avatar, title, subtitle, meta |
| `code/1`, `code_block/1` | Inline code and code blocks |
| `tooltip/1`, `popover/1` | Tooltips and popovers with Floating UI positioning |
| `toast/1`, `toast_container/1`, `push_toast/5` | Toast notifications with client-side rendering via JS hook |
| `flash/1`, `flash_group/1`, `flash_container/1`, `push_flash/5` | Flash messages — standard `@flash` compat + independent containers with markdown body and action buttons |
| `pager/1`, `load_more/1` | Pagination with page input, first/last buttons |

### JS Hooks

| Hook | Description |
|---|---|
| `PureAdminSettings` | Settings panel localStorage management |
| `PureAdminProfilePanel` | Profile panel tabs, favorites, click-outside |
| `PureAdminTooltip` | Tooltip positioning |
| `PureAdminPopover` | Popover positioning |
| `PureAdminToast` | Toast auto-dismiss |
| `PureAdminFlash` | Independent inline flash containers with markdown and action buttons |
| `PureAdminCommandPalette` | Command palette: multi-step commands (`/`), scoped search (`:`), keyboard nav |
| `PureAdminDetailPanel` | Detail panel toggle |
| `PureAdminSidebarResize` | Drag-to-resize sidebar |
| `PureAdminCharCounter` | Character counter with translatable messages |
| `PureAdminCheckbox` | Tri-state checkbox indeterminate sync |
| `PureAdminSplitButton` | Split button dropdown via Floating UI |
| `PureAdminSidebarSubmenu` | Sidebar submenu localStorage persistence |
| `PureAdminInfiniteScroll` | IntersectionObserver-based infinite scroll |

## CSS Framework

All classes follow the BEM pattern: `pa-{block}`, `pa-{block}--{modifier}`, `pa-{block}__{element}`.

Browse the live component showcase and theme previews at [pureadmin.io](https://pureadmin.io).

### Available Themes

| Theme | Package |
|---|---|
| Default | `@keenmate/pure-admin-core` |
| Audi | `@keenmate/pure-admin-theme-audi` |
| Corporate | `@keenmate/pure-admin-theme-corporate` |
| Dark | `@keenmate/pure-admin-theme-dark` |
| Express | `@keenmate/pure-admin-theme-express` |
| Minimal | `@keenmate/pure-admin-theme-minimal` |

### Installing Themes

Theme zips are self-contained — compiled CSS in `dist/` references fonts via relative paths (`../assets/fonts/...`), so extracting preserves correct asset resolution with no path adjustments needed. Each theme includes compiled CSS, SCSS source (for customization), bundled fonts, and a `theme.json` manifest.

#### Option A: Manual download

Download theme zips from [pureadmin.io](https://pureadmin.io) and extract them into `priv/static/themes/`:

```
priv/static/themes/
├── themes.json
├── audi/
│   ├── theme.json
│   ├── dist/audi.css
│   ├── scss/audi.scss
│   └── assets/fonts/*.woff2
├── dark/
│   ├── dist/dark.css
│   └── ...
└── ...
```

#### Option B: Pure Admin CLI

Install the [`@keenmate/pureadmin`](https://www.npmjs.com/package/@keenmate/pureadmin) CLI and manage themes in your project:

```bash
npm install -g @keenmate/pureadmin
pureadmin themes audi dark express    # download and extract
pureadmin update                      # re-download only changed themes
```

The CLI tracks versions and checksums in `pure-admin.json` — only changed themes are re-downloaded.

#### Option C: Download during CI/CD build

Fetch themes automatically in your Dockerfile using the bundle API:

```dockerfile
ARG THEMES_URL=https://pureadmin.io/api/bundle?themes=audi,dark,express,corporate,minimal
RUN apt-get update && apt-get install -y --no-install-recommends curl unzip && rm -rf /var/lib/apt/lists/* \
  && mkdir -p priv/static/themes \
  && curl -fsSL -o /tmp/themes.zip "${THEMES_URL}" \
  && unzip -o /tmp/themes.zip -d priv/static/themes \
  && rm -f /tmp/themes.zip
```

Pass a comma-separated list of theme names to the `themes` query parameter. The API returns a single zip with all requested themes. See `demo/Dockerfile` for a complete example.

#### Theme cache invalidation

The demo app's `ThemePlug` caches downloaded themes to disk. Each theme's `theme.json` contains a `checksums.content_sha` field — a SHA-256 hash of the package contents. On access, the plug validates the cache in the background by sending a conditional request (`If-None-Match: <content_sha>`) to pureadmin.io. If the server returns 200 (theme updated), it re-downloads without blocking the current request. Freshness checks are throttled to once per 10 minutes per theme.

To force-clear the cache:

```bash
make themes-clear
```

## Translations (i18n)

All user-facing strings in components are translatable via a runtime callback. Without configuration, English defaults are used.

```elixir
# config/config.exs
config :keen_pure_admin,
  translate: &MyApp.Translations.translate/2
```

The callback receives a flat key and a params map:

```elixir
defmodule MyApp.Translations do
  def translate(key, params) do
    # Load from DB, Gettext, ETS — whatever fits your app
    translation = MyApp.Repo.get_translation(key, current_locale())
    PureAdmin.Translations.interpolate(translation, params)
  end
end
```

Keys follow the `pureAdmin.*` convention (e.g., `pureAdmin.buttons.cancel`, `pureAdmin.pagination.nextPage`, `pureAdmin.commandPalette.searching`). See `PureAdmin.Translations.defaults()` for the full list.

## Page Context

Server-rendered JSON in a hidden input, available to JS synchronously — no API fetch needed. CSP-safe.

```heex
<%!-- In your root layout --%>
<.page_context />
```

Register providers via config:

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

Each provider receives assigns and returns a map merged into the context. The settings panel reads `themeManifests` from the context automatically (falls back to API if missing). See `PureAdmin.PageContext` for details.

## Logging

All JS hooks use a categorized logger — silent by default, zero overhead in production.

```javascript
// Enable in browser console
PureAdmin.logging.enableLogging()

// Or per-category
PureAdmin.logging.setCategoryLevel('PA:SETTINGS', 'debug')

// List categories
PureAdmin.logging.getCategories()
// => ["PA:SETTINGS", "PA:CMD_PALETTE", ...]
```

Also available via `window.components['keen-pure-admin'].logging` (KeenMate convention).

## Requirements

- Elixir ~> 1.15
- Phoenix LiveView ~> 1.0
- `@keenmate/pure-admin-core` CSS (v2.3.5+)

## Development

```bash
mix deps.get      # Install dependencies
mix compile        # Compile
mix test           # Run tests
mix format         # Format code
mix quality        # Format check + credo + dialyzer
```

### Demo App

```bash
cd demo
mix setup         # Install deps + build assets
mix phx.server    # Visit http://localhost:4000
```

### Running the Demo with Podman

Using Make (recommended):

```bash
make podman-build     # Build the image
make podman-run       # Run the container (port 4000)
make podman-deploy    # Build + run in one step
make podman-push      # Push to registry.km8.es
make podman-logs      # Tail container logs
make podman-stop      # Stop the container
make podman-clean     # Remove container and image
```

Or manually:

```bash
podman build -f demo/Dockerfile -t keen-pure-admin-demo .
podman run -p 4000:4000 \
  -e SECRET_KEY_BASE=$(mix phx.gen.secret) \
  -e PHX_HOST=localhost \
  keen-pure-admin-demo
```

For production (`elixir.demo.pureadmin.io`):

```bash
SECRET_KEY_BASE=<your-secret> PHX_HOST=elixir.demo.pureadmin.io make podman-deploy
```

## License

MIT