diff --git a/lib/cobblemon_ui_web/live/battle_components.ex b/lib/cobblemon_ui_web/live/battle_components.ex
new file mode 100644
index 0000000..7a1b5c1
--- /dev/null
+++ b/lib/cobblemon_ui_web/live/battle_components.ex
@@ -0,0 +1,84 @@
+defmodule CobblemonUiWeb.BattleComponents do
+ use CobblemonUiWeb, :html
+
+ attr :battle, :map, required: true
+ attr :player_id, :string, required: true
+
+ def battle_panel(assigns) do
+ ~H"""
+
-
-
- Active Battle
-
-
-
-
- VS
-
-
- <%= for actor <- @battle.actors do %>
-
-
-
- <%= if actor.type == "player" do %>
- <.icon name="hero-user" class="size-3.5 text-primary" />
- <% else %>
- <.icon name="hero-cpu-chip" class="size-3.5 text-warning" />
- <% end %>
-
-
-
{actor.name}
-
- {actor.type}
-
-
-
- <%= for poke <- actor.active_pokemon do %>
-
-
-
- {poke.species}
-
- Lv.{poke.level}
-
-
-
{poke.hp}/{poke.max_hp}
-
-
-
"bg-base-content/20"
- poke.hp / poke.max_hp > 0.5 -> "bg-success"
- poke.hp / poke.max_hp > 0.2 -> "bg-warning"
- true -> "bg-error"
- end
- ]}
- style={"width: #{if poke.max_hp > 0, do: Float.round(poke.hp / poke.max_hp * 100, 1), else: 0}%"}
- />
-
-
- <% end %>
-
- <% end %>
-
-
- """
- end
-
# --- Helpers ---
defp party_count(%{party: party}), do: Enum.count(party, &(not is_nil(&1)))
@@ -694,38 +347,6 @@ defmodule CobblemonUiWeb.DashboardLive do
offset + slot_index
end
- defp gender_symbol("male"), do: "♂"
- defp gender_symbol("female"), do: "♀"
- defp gender_symbol(_), do: "—"
-
- defp gender_color("male"), do: "text-info"
- defp gender_color("female"), do: "text-error"
- defp gender_color(_), do: "text-base-content/40"
-
- defp format_move(move) when is_binary(move), do: String.replace(move, "_", " ")
- defp format_move(_), do: "—"
-
- defp format_stat(:hp), do: "HP"
- defp format_stat(:attack), do: "ATK"
- defp format_stat(:defense), do: "DEF"
- defp format_stat(:special_attack), do: "SPA"
- defp format_stat(:special_defense), do: "SPD"
- defp format_stat(:speed), do: "SPE"
- defp format_stat(other), do: to_string(other)
-
- defp stat_list(stats) when is_map(stats) do
- [:hp, :attack, :defense, :special_attack, :special_defense, :speed]
- |> Enum.map(fn key -> {key, Map.get(stats, key, 0)} end)
- end
-
- defp stat_list(_), do: []
-
- defp ev_total(evs) when is_map(evs) do
- evs |> Map.values() |> Enum.sum()
- end
-
- defp ev_total(_), do: 0
-
defp player_name(players, uuid) do
case Enum.find(players, fn p -> p.uuid == uuid end) do
%{name: name} when is_binary(name) -> name
diff --git a/lib/cobblemon_ui_web/live/pokemon_components.ex b/lib/cobblemon_ui_web/live/pokemon_components.ex
new file mode 100644
index 0000000..9f41ba6
--- /dev/null
+++ b/lib/cobblemon_ui_web/live/pokemon_components.ex
@@ -0,0 +1,292 @@
+defmodule CobblemonUiWeb.PokemonComponents do
+ use CobblemonUiWeb, :html
+
+ attr :pokemon, :map, required: true
+ attr :index, :integer, required: true
+ attr :compact, :boolean, default: false
+ attr :tier, :string, default: nil
+
+ def pokemon_card(%{pokemon: nil} = assigns) do
+ ~H"""
+
+ Empty
+
+ """
+ end
+
+ def pokemon_card(assigns) do
+ ~H"""
+
+
+
+
+ {@pokemon.species || "Unknown"}
+
+
+ Lv. {@pokemon.level || "?"}
+
+
+
+ <.tier_badge :if={@tier} tier={@tier} species={@pokemon.species} compact={@compact} />
+
+ <.icon
+ name="hero-sparkles"
+ class={[
+ "text-warning",
+ if(@compact, do: "size-3", else: "size-4")
+ ]}
+ />
+
+
+
+
+
+ {@pokemon.nature}
+
+
+ {gender_symbol(@pokemon.gender)}
+
+
+
+ """
+ end
+
+ attr :tier, :string, required: true
+ attr :species, :string, required: true
+ attr :compact, :boolean, default: false
+
+ def tier_badge(assigns) do
+ ~H"""
+
"bg-red-500/20 text-red-400 ring-1 ring-red-500/30 hover:bg-red-500/30"
+ "A" -> "bg-orange-500/20 text-orange-400 ring-1 ring-orange-500/30 hover:bg-orange-500/30"
+ "B" -> "bg-yellow-500/20 text-yellow-400 ring-1 ring-yellow-500/30 hover:bg-yellow-500/30"
+ "C" -> "bg-green-500/20 text-green-400 ring-1 ring-green-500/30 hover:bg-green-500/30"
+ "D" -> "bg-blue-500/20 text-blue-400 ring-1 ring-blue-500/30 hover:bg-blue-500/30"
+ _ -> "bg-base-300/30 text-base-content/40 ring-1 ring-base-300/40"
+ end
+ ]}
+ >
+ {@tier}
+
+ """
+ end
+
+ attr :pokemon, :map, required: true
+ attr :tier, :string, default: nil
+
+ def pokemon_detail(assigns) do
+ ~H"""
+
+ <%!-- Detail header --%>
+
+
+
+ <.icon name="hero-bolt" class="size-4 text-primary/70" />
+
+
+
+ {@pokemon.species || "Unknown"}
+ <.tier_badge :if={@tier} tier={@tier} species={@pokemon.species} />
+ ★
+
+
+ Level {@pokemon.level || "?"} · {String.capitalize(@pokemon.form || "default")} form
+
+
+
+
+
+
+
+ <%!-- Info Column --%>
+
+
+
+ Details
+
+
+ <.stat_pill label="Nature" value={@pokemon.nature} />
+ <.stat_pill label="Ability" value={@pokemon.ability} />
+ <.stat_pill label="Gender" value={@pokemon.gender} />
+ <.stat_pill label="Friendship" value={@pokemon.friendship} />
+
+
+
+ <%!-- Moves --%>
+
+
+ Moves
+
+
+
+ {format_move(move)}
+
+
+ No moves
+
+
+
+
+
+ <%!-- Stats Column --%>
+
+ <%!-- IVs --%>
+
+
+ IVs
+
+
+ <.stat_bar
+ :for={{stat, val} <- stat_list(@pokemon.ivs)}
+ label={format_stat(stat)}
+ value={val}
+ max={31}
+ />
+
+
+
+ <%!-- EVs --%>
+
+
+ EVs
+
+ ({ev_total(@pokemon.evs)}/510)
+
+
+
+ <.stat_bar
+ :for={{stat, val} <- stat_list(@pokemon.evs)}
+ label={format_stat(stat)}
+ value={val}
+ max={252}
+ />
+
+
+
+
+
+ """
+ end
+
+ attr :label, :string, required: true
+ attr :value, :any, required: true
+
+ def stat_pill(assigns) do
+ ~H"""
+
+
{@label}
+
{@value || "—"}
+
+ """
+ end
+
+ attr :label, :string, required: true
+ attr :value, :integer, required: true
+ attr :max, :integer, required: true
+
+ def stat_bar(assigns) do
+ pct = if assigns.max > 0, do: min(assigns.value / assigns.max * 100, 100), else: 0
+
+ color =
+ cond do
+ pct >= 90 -> "bg-success"
+ pct >= 60 -> "bg-info"
+ pct >= 30 -> "bg-warning"
+ true -> "bg-error/70"
+ end
+
+ assigns = assign(assigns, pct: pct, color: color)
+
+ ~H"""
+
+
+ {@label}
+
+
+
+ {@value}
+
+
+ """
+ end
+
+ # --- Helpers ---
+
+ defp gender_symbol("male"), do: "♂"
+ defp gender_symbol("female"), do: "♀"
+ defp gender_symbol(_), do: "—"
+
+ defp gender_color("male"), do: "text-info"
+ defp gender_color("female"), do: "text-error"
+ defp gender_color(_), do: "text-base-content/40"
+
+ defp format_move(move) when is_binary(move), do: String.replace(move, "_", " ")
+ defp format_move(_), do: "—"
+
+ defp format_stat(:hp), do: "HP"
+ defp format_stat(:attack), do: "ATK"
+ defp format_stat(:defense), do: "DEF"
+ defp format_stat(:special_attack), do: "SPA"
+ defp format_stat(:special_defense), do: "SPD"
+ defp format_stat(:speed), do: "SPE"
+ defp format_stat(other), do: to_string(other)
+
+ defp stat_list(stats) when is_map(stats) do
+ [:hp, :attack, :defense, :special_attack, :special_defense, :speed]
+ |> Enum.map(fn key -> {key, Map.get(stats, key, 0)} end)
+ end
+
+ defp stat_list(_), do: []
+
+ defp ev_total(evs) when is_map(evs), do: evs |> Map.values() |> Enum.sum()
+ defp ev_total(_), do: 0
+end