# `PhoenixKit.Dashboard.Badge`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.87/lib/phoenix_kit/dashboard/badge.ex#L1)

Defines badge types for dashboard tab indicators.

Badges provide visual feedback on tabs to show counts, status, or draw attention.
They can be static or update live via PubSub subscriptions.

## Badge Types

- `:count` - Numeric count badge (e.g., "5", "99+")
- `:dot` - Simple colored dot indicator
- `:status` - Status indicator with color (green/yellow/red)
- `:new` - "New" text indicator
- `:text` - Custom text badge

## Static Badge

    %Badge{type: :count, value: 5}
    %Badge{type: :dot, color: :success}
    %Badge{type: :status, value: :online, color: :success}

## Live Badge with PubSub

    %Badge{
      type: :count,
      subscribe: {"farm:stats", fn msg -> msg.printing_count end}
    }

## Context-Aware Badge

For badges that show different values per user/organization/context, use `context_key`
and optionally `loader`. The badge value is stored per-context in socket assigns
instead of globally.

    # Badge depends on current organization context
    %Badge{
      type: :count,
      context_key: :organization,
      loader: {MyApp.Alerts, :count_for_org},  # Called with (context)
      subscribe: "org:{id}:alerts"  # {id} replaced with context.id
    }

### Context Placeholders

Subscribe topics support `{field}` placeholders that are resolved from the current context:
- `{id}` - context.id or context[:id]
- `{name}` - context.name or context[:name]
- Any field accessible on the context struct/map

### Loader Function

The loader is called when the LiveView mounts to get the initial badge value:
- `{Module, :function}` - Called as `Module.function(context)`
- `fn context -> value end` - Anonymous function

## Badge with Attention Animation

    %Badge{
      type: :count,
      value: 3,
      color: :error,
      pulse: true
    }

# `badge_color`

```elixir
@type badge_color() ::
  :primary
  | :secondary
  | :accent
  | :info
  | :success
  | :warning
  | :error
  | :neutral
```

# `badge_type`

```elixir
@type badge_type() :: :count | :dot | :status | :new | :text | :compound
```

# `compound_segment`

```elixir
@type compound_segment() :: %{
  :value =&gt; integer() | String.t(),
  :color =&gt; badge_color(),
  optional(:label) =&gt; String.t()
}
```

# `compound_style`

```elixir
@type compound_style() :: :text | :blocks | :dots
```

# `loader_config`

```elixir
@type loader_config() :: {module(), atom()} | (any() -&gt; any())
```

# `subscribe_config`

```elixir
@type subscribe_config() ::
  {String.t(), (map() -&gt; any())} | {String.t(), atom()} | String.t()
```

# `t`

```elixir
@type t() :: %PhoenixKit.Dashboard.Badge{
  animate: boolean(),
  color: badge_color(),
  compound_style: compound_style(),
  context_key: atom() | nil,
  format: (any() -&gt; String.t()) | nil,
  hidden_when_zero: boolean(),
  hide_zero_segments: boolean(),
  loader: loader_config() | nil,
  max: integer() | nil,
  metadata: map(),
  pulse: boolean(),
  segments: [compound_segment()],
  separator: String.t(),
  subscribe: subscribe_config() | nil,
  type: badge_type(),
  value: any()
}
```

# `color_class`

```elixir
@spec color_class(t()) :: String.t()
```

Returns the CSS color class for the badge.

# `compound`

```elixir
@spec compound(
  [compound_segment()],
  keyword()
) :: t()
```

Creates a compound badge with multiple colored segments.

A compound badge displays multiple values with different colors in a single badge.
Useful for showing status breakdowns like "10 success / 5 pending / 2 error".

## Styles

- `:text` (default) - Colored text values with separator (e.g., "10 / 5 / 2")
- `:blocks` - Colored background pills side by side
- `:dots` - Colored dots with numbers beside them

## Options

- `:style` - Display style: `:text`, `:blocks`, `:dots` (default: `:text`)
- `:separator` - Separator string for `:text` style (default: "/")
- `:hide_zero_segments` - Hide segments with value 0 (default: false)
- `:pulse` - Enable pulse animation (default: false)

## Segment Format

Each segment is a map with:
- `:value` (required) - Integer or string value to display
- `:color` (required) - Badge color atom (:success, :warning, :error, etc.)
- `:label` (optional) - Text label shown after value (e.g., "done", "pending")

## Examples

    # Simple compound badge
    Badge.compound([
      %{value: 10, color: :success},
      %{value: 5, color: :warning},
      %{value: 2, color: :error}
    ])

    # With labels and blocks style
    Badge.compound([
      %{value: 10, color: :success, label: "done"},
      %{value: 5, color: :warning, label: "pending"}
    ], style: :blocks)

    # Hide zero segments
    Badge.compound([
      %{value: 10, color: :success},
      %{value: 0, color: :error}
    ], hide_zero_segments: true)
    # Only shows: "10"

# `compound_context`

```elixir
@spec compound_context(atom(), loader_config(), keyword()) :: t()
```

Creates a context-aware compound badge that loads segments per-context.

The loader function should return a list of segment maps.

## Examples

    # Loader returns list of segments for current organization
    Badge.compound_context(:organization, {MyApp.Tasks, :get_status_counts},
      style: :blocks
    )

    # In MyApp.Tasks
    def get_status_counts(org) do
      [
        %{value: count_completed(org.id), color: :success},
        %{value: count_pending(org.id), color: :warning},
        %{value: count_overdue(org.id), color: :error}
      ]
    end

# `context`

```elixir
@spec context(atom(), loader_config(), keyword()) :: t()
```

Creates a context-aware badge that loads values per-context.

## Examples

    # Badge that shows alert count for current organization
    Badge.context(:organization, {MyApp.Alerts, :count_for_org}, color: :error)

    # With live updates via context-specific PubSub topic
    Badge.context(:farm, {MyApp.Farms, :printing_count},
      subscribe: "farm:{id}:stats",
      color: :info
    )

# `context_aware?`

```elixir
@spec context_aware?(t()) :: boolean()
```

Checks if this badge is context-aware (requires per-context value storage).

# `count`

```elixir
@spec count(
  integer(),
  keyword()
) :: t()
```

Creates a count badge.

## Examples

    Badge.count(5)
    Badge.count(10, color: :error, max: 99)

# `display_value`

```elixir
@spec display_value(t()) :: String.t() | nil
```

Formats the badge value for display.

## Examples

    iex> badge = Badge.count(5)
    iex> Badge.display_value(badge)
    "5"

    iex> badge = Badge.count(150, max: 99)
    iex> Badge.display_value(badge)
    "99+"

# `dot`

```elixir
@spec dot(keyword()) :: t()
```

Creates a dot badge.

## Examples

    Badge.dot()
    Badge.dot(color: :success)
    Badge.dot(color: :error, pulse: true)

# `dot_color_class`

```elixir
@spec dot_color_class(t()) :: String.t()
```

Returns the CSS background color class for dot badges.

# `extract_value`

```elixir
@spec extract_value(t(), map()) :: any()
```

Extracts value from a PubSub message using the badge's subscription config.

# `get_resolved_topic`

```elixir
@spec get_resolved_topic(t(), map() | struct() | nil) :: String.t() | nil
```

Gets the resolved topic for this badge given a context.

For context-aware badges, resolves placeholders. For regular badges, returns the topic as-is.

# `get_topic`

```elixir
@spec get_topic(t()) :: String.t() | nil
```

Gets the PubSub topic for this badge, if it has a subscription.

# `live`

```elixir
@spec live(String.t(), atom() | (map() -&gt; any()), keyword()) :: t()
```

Creates a live badge that subscribes to PubSub updates.

## Examples

    Badge.live("user:notifications", :count)
    Badge.live("farm:stats", fn msg -> msg.printing_count end, color: :info)

# `live?`

```elixir
@spec live?(t()) :: boolean()
```

Checks if this badge has a live subscription.

# `load_value`

```elixir
@spec load_value(t(), map() | struct() | nil) :: any()
```

Loads the initial badge value using the loader function.

## Examples

    iex> badge = Badge.context(:org, {MyApp.Alerts, :count_for_org})
    iex> Badge.load_value(badge, %{id: 123})
    5  # Result from MyApp.Alerts.count_for_org(%{id: 123})

# `new`

```elixir
@spec new(map() | keyword()) :: {:ok, t()} | {:error, String.t()}
```

Creates a new Badge struct from a map or keyword list.

## Options

- `:type` - Badge type: :count, :dot, :status, :new, :text (default: :count)
- `:value` - The value to display (number for count, atom/string for status/text)
- `:color` - Badge color: :primary, :secondary, :accent, :info, :success, :warning, :error, :neutral (default: :primary)
- `:max` - Maximum display value for count badges (e.g., 99 shows "99+") (optional)
- `:pulse` - Enable pulse animation (default: false)
- `:animate` - Enable value change animation (default: true)
- `:hidden_when_zero` - Hide count badge when value is 0 (default: true)
- `:subscribe` - PubSub subscription config for live updates (optional)
- `:format` - Custom formatter function for the value (optional)
- `:metadata` - Custom metadata map (default: %{})
- `:context_key` - Context selector key for per-context badges (optional, e.g., :organization)
- `:loader` - Function to load initial value for context: `{Module, :function}` or `fn context -> value end`

## Subscribe Configuration

The `:subscribe` option can be:

1. A tuple of {topic, extractor_function}:
   `{"farm:stats", fn msg -> msg.printing_count end}`

2. A tuple of {topic, key_atom} to extract from message:
   `{"farm:stats", :printing_count}`

3. Just a topic string (uses full message as value):
   `"user:notifications:count"`

Topics support `{field}` placeholders for context-aware badges:
   `"org:{id}:alerts"` - resolves to `"org:123:alerts"` when context.id is 123

## Examples

    iex> Badge.new(type: :count, value: 5)
    {:ok, %Badge{type: :count, value: 5}}

    iex> Badge.new(type: :dot, color: :error, pulse: true)
    {:ok, %Badge{type: :dot, color: :error, pulse: true}}

    iex> Badge.new(type: :count, subscribe: {"orders:count", :count})
    {:ok, %Badge{type: :count, subscribe: {"orders:count", :count}}}

    # Context-aware badge
    iex> Badge.new(type: :count, context_key: :organization, loader: {MyApp.Alerts, :count_for_org})
    {:ok, %Badge{type: :count, context_key: :organization, loader: {MyApp.Alerts, :count_for_org}}}

# `new!`

```elixir
@spec new!(map() | keyword()) :: t()
```

Creates a new Badge struct, raising on error.

# `new_indicator`

```elixir
@spec new_indicator(keyword()) :: t()
```

Creates a "New" badge.

## Examples

    Badge.new_indicator()
    Badge.new_indicator(color: :accent)

# `resolve_topic`

```elixir
@spec resolve_topic(String.t() | nil, map() | struct()) :: String.t() | nil
```

Resolves placeholders in a topic string using context data.

Supports `{field}` syntax where field is accessed from the context.

## Examples

    iex> Badge.resolve_topic("org:{id}:alerts", %{id: 123})
    "org:123:alerts"

    iex> Badge.resolve_topic("farm:{farm_id}:stats", %{farm_id: "abc"})
    "farm:abc:stats"

# `status`

```elixir
@spec status(
  atom() | String.t(),
  keyword()
) :: t()
```

Creates a status badge.

## Examples

    Badge.status(:online, color: :success)
    Badge.status(:busy, color: :warning)
    Badge.status(:offline, color: :error)

# `text`

```elixir
@spec text(
  String.t(),
  keyword()
) :: t()
```

Creates a text badge with custom text.

## Examples

    Badge.text("Beta")
    Badge.text("Pro", color: :accent)

# `update_segments`

```elixir
@spec update_segments(t(), [compound_segment()]) :: t()
```

Updates segments for a compound badge.

# `update_value`

```elixir
@spec update_value(t(), any()) :: t()
```

Updates the badge value.

# `visible?`

```elixir
@spec visible?(t()) :: boolean()
```

Checks if the badge should be visible.

Count badges with value 0 are hidden by default when hidden_when_zero is true.
Compound badges are hidden when all segments have zero value (and hide_zero_segments is true).

# `visible_segments`

```elixir
@spec visible_segments(t()) :: [compound_segment()]
```

Returns visible segments for a compound badge.

If `hide_zero_segments` is true, filters out segments with value 0 or nil.

## Examples

    badge = Badge.compound([
      %{value: 10, color: :success},
      %{value: 0, color: :error}
    ], hide_zero_segments: true)

    Badge.visible_segments(badge)
    # => [%{value: 10, color: :success}]

---

*Consult [api-reference.md](api-reference.md) for complete listing*
