defmodule PhiaUi.Components.AdvancedSelects do @moduledoc """ Advanced select components for PhiaUI. Provides three higher-level selection patterns beyond the native `` carries the selected value for form submission. """ def tree_select(assigns) do selected_label = find_label(assigns.options, assigns.value) assigns = assign(assigns, :selected_label, selected_label) ~H"""
""" end # --------------------------------------------------------------------------- # tree_select_option/1 # --------------------------------------------------------------------------- attr(:option, :map, required: true, doc: "Option map: `%{label, value, children: [...]}`.") attr(:selected_value, :string, default: nil, doc: "Currently selected value.") attr(:level, :integer, default: 0, doc: "Nesting depth (0 = root).") attr(:id_prefix, :string, default: "ts", doc: "ID prefix for expand toggles.") @doc """ Renders a single tree node: expand toggle (if children) + clickable label. Children are rendered recursively, initially hidden (managed by JS hook). """ def tree_select_option(assigns) do children = Map.get(assigns.option, :children, []) has_children = children != [] opt_value = Map.get(assigns.option, :value, "") opt_label = Map.get(assigns.option, :label, "") is_active = opt_value == assigns.selected_value assigns = assigns |> assign(:children, children) |> assign(:has_children, has_children) |> assign(:opt_value, opt_value) |> assign(:opt_label, opt_label) |> assign(:is_active, is_active) ~H"""
<%= if @has_children do %> <% else %> <% end %> {@opt_label}
""" end # --------------------------------------------------------------------------- # rich_select/1 # --------------------------------------------------------------------------- attr(:id, :string, default: nil, doc: "HTML id for the fieldset.") attr(:value, :string, default: nil, doc: "Currently selected option value." ) attr(:name, :string, default: "rich_select", doc: "Name attribute for the hidden radio inputs." ) attr(:on_change, :string, default: nil, doc: "LiveView event name fired via `phx-click` when an option is selected." ) attr(:placeholder, :string, default: nil, doc: "Optional prompt text shown above the options list (aria-label fallback)." ) attr(:options, :list, default: [], doc: """ List of option maps. Supported keys: - `:value` (required) — option value string - `:label` (required) — display title - `:description` — optional subtitle text - `:icon` — Lucide icon name string - `:avatar_src` — URL for an avatar image """ ) attr(:size, :atom, values: [:sm, :default, :lg], default: :default, doc: "Controls option row height and text size." ) attr(:class, :string, default: nil, doc: "Additional CSS classes.") @doc """ Renders a visible list of rich radio options — each with avatar/icon + title + description. Pure HEEx, no JS required. Use `phx-click` or wrap in a Phoenix form for submission. """ def rich_select(assigns) do ~H"""
<%= for opt <- @options do %> <% opt_val = Map.get(opt, :value, "") opt_label = Map.get(opt, :label, "") opt_desc = Map.get(opt, :description) opt_icon = Map.get(opt, :icon) opt_avatar = Map.get(opt, :avatar_src) is_active = opt_val == @value input_id = "#{@id || @name}-#{opt_val}" %> <% end %>
""" end # --------------------------------------------------------------------------- # rich_select_option/1 # --------------------------------------------------------------------------- attr(:value, :string, required: true, doc: "Option value.") attr(:label, :string, required: true, doc: "Display title.") attr(:description, :string, default: nil, doc: "Optional subtitle text.") attr(:icon, :string, default: nil, doc: "Lucide icon name.") attr(:avatar_src, :string, default: nil, doc: "Avatar image URL.") attr(:selected, :boolean, default: false, doc: "Whether this option is currently selected.") attr(:name, :string, default: "rich_select", doc: "Radio input name.") attr(:on_change, :string, default: nil, doc: "LiveView event name.") attr(:class, :string, default: nil, doc: "Additional CSS classes.") @doc """ Standalone rich select option row — for composable usage outside `rich_select/1`. """ def rich_select_option(assigns) do input_id = "rs-#{assigns.name}-#{assigns.value}" assigns = assign(assigns, :input_id, input_id) ~H""" """ end # --------------------------------------------------------------------------- # visual_select/1 # --------------------------------------------------------------------------- attr(:id, :string, default: nil, doc: "HTML id for the fieldset.") attr(:name, :string, required: true, doc: "Name attribute for the radio inputs.") attr(:value, :string, default: nil, doc: "Currently selected option value.") attr(:cols, :integer, default: 3, doc: "Number of grid columns (2, 3, or 4)." ) attr(:options, :list, default: [], doc: """ List of option maps. Supported keys: - `:value` (required) - `:label` — display text below the tile - `:icon` — Lucide icon name (shown if no image) - `:image_src` — image URL for the tile thumbnail - `:description` — small descriptive text """ ) attr(:class, :string, default: nil, doc: "Additional CSS classes on the grid container.") @doc """ Renders a grid of visual radio-backed option tiles. Each tile can display an image, an icon, a label, and a description. Pure HEEx — no JS required. """ def visual_select(assigns) do ~H"""
<%= for opt <- @options do %> <% opt_val = Map.get(opt, :value, "") opt_label = Map.get(opt, :label) opt_desc = Map.get(opt, :description) opt_img = Map.get(opt, :image_src) is_active = opt_val == @value input_id = "#{@id || @name}-vs-#{opt_val}" %> <% end %>
""" end # --------------------------------------------------------------------------- # visual_select_item/1 # --------------------------------------------------------------------------- attr(:value, :string, required: true, doc: "Option value.") attr(:label, :string, default: nil, doc: "Display label below the tile.") attr(:image_src, :string, default: nil, doc: "Thumbnail image URL.") attr(:description, :string, default: nil, doc: "Small descriptive text.") attr(:selected, :boolean, default: false, doc: "Whether this item is currently selected.") attr(:name, :string, default: "visual_select", doc: "Radio input name.") attr(:class, :string, default: nil, doc: "Additional CSS classes.") @doc """ Standalone visual select tile — for composable usage outside `visual_select/1`. """ def visual_select_item(assigns) do input_id = "vsi-#{assigns.name}-#{assigns.value}" assigns = assign(assigns, :input_id, input_id) ~H""" """ end # --------------------------------------------------------------------------- # Private helpers # --------------------------------------------------------------------------- defp grid_cols_class(2), do: "grid grid-cols-2" defp grid_cols_class(4), do: "grid grid-cols-4" defp grid_cols_class(_), do: "grid grid-cols-3" defp rich_size_class(:sm), do: "py-1.5" defp rich_size_class(:lg), do: "py-3" defp rich_size_class(_), do: "py-2" defp rich_avatar_class(:sm), do: "h-6 w-6" defp rich_avatar_class(:lg), do: "h-10 w-10" defp rich_avatar_class(_), do: "h-8 w-8" defp find_label([], _value), do: nil defp find_label([opt | rest], value) do opt_value = Map.get(opt, :value) opt_label = Map.get(opt, :label) children = Map.get(opt, :children, []) cond do opt_value == value -> opt_label children != [] -> find_label(children, value) || find_label(rest, value) true -> find_label(rest, value) end end end