defmodule Corex.Avatar do
@moduledoc ~S'''
Phoenix implementation of [Zag.js Avatar](https://zagjs.com/components/react/avatar).
## Anatomy
### Fallback
```heex
<.avatar src="" class="avatar">
<:fallback>
AB
```
### Value slot
Override fallback body and receive the current `src` as `value`. When `:value` is omitted, `<:fallback>` is used.
```heex
<.avatar src="https://corex-ui.com/images/avatar.png" alt="" class="avatar">
<:value :let={src}>
{if src, do: "IMG", else: " - "}
```
### Pending
With `pending={true}`, the hook and `` are not mounted until `pending={false}`. Use `:loading` or `avatar_skeleton/1`.
```heex
<.avatar pending class="avatar">
<:loading>Loading
```
## API
Requires a stable `id` on `<.avatar>`.
| Function | Action | Returns |
| -------- | ------ | ------- |
| [`set_src/2`](#set_src/2) | Set image URL (client) | `%Phoenix.LiveView.JS{}` |
| [`set_src/3`](#set_src/3) | Set image URL (server) | `socket` |
| [`loaded/1`](#loaded/1) | Read loaded state (client) | `%Phoenix.LiveView.JS{}` |
| [`loaded/2`](#loaded/2) | Read loaded state with `respond_to` (client) | `%Phoenix.LiveView.JS{}` |
| [`loaded/3`](#loaded/3) | Read loaded state (server) | `socket` |
For `loaded`, use `respond_to: :server | :client | :both`. LiveView receives `avatar_loaded_response`; the DOM receives `avatar-loaded`.
## Events
Pick an event name and pass it to `on_*` on `<.avatar>`.
### Server events
| Event | When | Payload |
| ----- | ---- | ------- |
| `on_status_change="avatar_status_changed"` | Image load status changes | `%{"id" => id, "status" => "loaded" \| "error"}` |
### on_status_change
```heex
<.avatar
class="avatar"
src="https://corex-ui.com/images/avatar.png"
alt="Avatar"
on_status_change="avatar_status_changed"
>
<:fallback>JD
```
```elixir
def handle_event("avatar_status_changed", %{"id" => _id, "status" => status}, socket) do
{:noreply, assign(socket, :avatar_status, status)}
end
```
### Client events
| Event | When | `event.detail` |
| ----- | ---- | -------------- |
| `on_status_change_client="avatar-status-changed"` | Image load status changes | `id`, `status` |
### on_status_change_client
```heex
<.avatar
id="avatar-events-client"
class="avatar"
src="https://corex-ui.com/images/avatar.png"
on_status_change_client="avatar-status-changed"
>
<:fallback>JD
```
```javascript
const el = document.getElementById("avatar-events-client");
el?.addEventListener("avatar-status-changed", (e) => console.log(e.detail));
```
## Style
Target parts with `data-scope` and `data-part`, or use Corex Design: import tokens and `avatar.css`, then set `class="avatar"` on `<.avatar>`.
```css
[data-scope="avatar"][data-part="root"] {}
[data-scope="avatar"][data-part="image"] {}
[data-scope="avatar"][data-part="fallback"] {}
[data-scope="avatar"][data-part="skeleton"] {}
```
```css
@import "../corex/main.css";
@import "../corex/tokens/themes/neo/light.css";
@import "../corex/components/avatar.css";
```
Stack modifiers on the host (`class` on `<.avatar>`).
### Color
| Modifier | Classes |
| -------- | ------- |
| Default | `avatar` |
| Accent | `avatar avatar--accent` |
| Brand | `avatar avatar--brand` |
| Alert | `avatar avatar--alert` |
| Info | `avatar avatar--info` |
| Success | `avatar avatar--success` |
### Size
| Modifier | Classes |
| -------- | ------- |
| SM | `avatar avatar--sm` |
| MD | `avatar avatar--md` |
| LG | `avatar avatar--lg` |
| XL | `avatar avatar--xl` |
'''
@doc type: :component
use Phoenix.Component
import Corex.Api.Doc
alias Corex.Avatar.Anatomy.{Fallback, Image, Props, Root, Skeleton}
alias Corex.Avatar.Connect
alias Phoenix.LiveView
alias Phoenix.LiveView.JS
import Corex.Helpers, only: [respond_to_fields: 1]
attr(:id, :string, required: false, doc: "The id of the avatar")
attr(:src, :string, default: nil, doc: "Image source URL")
attr(:alt, :string, default: "", doc: "Alternative text for the image")
attr(:dir, :string, default: nil, values: [nil, "ltr", "rtl"], doc: "Direction")
attr(:pending, :boolean,
default: false,
doc:
"When true, renders only the loading UI (`:loading` slot or `avatar_skeleton/1`), without the Avatar hook."
)
attr(:on_status_change, :string,
default: nil,
doc: "Server event when image load status changes"
)
attr(:on_status_change_client, :string,
default: nil,
doc: "Client event when image load status changes"
)
attr(:rest, :global)
slot :loading,
required: false,
doc: "Custom loading content when `pending` is true. Overrides default `avatar_skeleton/1`." do
attr(:class, :string, required: false)
end
slot :fallback,
required: false,
doc: "Content for the fallback part when the image is absent or not yet shown." do
attr(:class, :string, required: false)
end
slot :value,
required: false,
doc:
"Optional replacement for `:fallback` inner content. Use `:let={value}` - `value` is the image `src` (or `nil`)." do
attr(:class, :string, required: false)
end
def avatar(assigns) do
assigns =
assigns
|> assign_new(:id, fn -> "avatar-#{System.unique_integer([:positive])}" end)
|> assign_new(:dir, fn -> nil end)
~H"""