Packages

phoenix_kit

2.37.3
2.60.2 2.60.1 2.60.0 2.59.0 2.58.0 2.57.1 2.57.0 2.56.1 2.56.0 2.55.1 2.55.0 2.54.2 2.54.1 2.54.0 2.53.0 2.52.2 2.52.1 2.52.0 2.51.0 2.50.0 2.49.1 2.49.0 2.48.0 2.47.0 2.46.0 2.45.0 2.44.0 2.43.1 2.43.0 2.42.1 2.42.0 2.41.6 2.41.4 2.41.3 2.41.2 2.41.1 2.41.0 2.40.1 2.40.0 2.39.0 2.38.1 2.38.0 2.37.5 2.37.4 2.37.3 2.37.2 2.37.1 2.37.0 2.36.1 2.36.0 2.35.0 2.34.0 2.33.0 2.32.1 2.32.0 2.31.1 2.31.0 2.30.0 2.29.1 2.29.0 2.28.2 2.28.1 2.28.0 2.27.2 2.27.1 2.27.0 2.26.1 2.26.0 2.25.0 2.24.0 2.23.3 2.23.2 2.23.1 2.23.0 2.22.24 2.22.23 2.22.22 2.22.21 2.22.20 2.22.19 2.22.18 2.22.17 2.22.16 2.22.15 2.22.14 2.22.13 2.22.12 2.22.11 2.22.10 2.22.9 2.22.8 2.22.7 2.22.6 2.22.5 2.22.4 2.22.3 2.22.2 2.22.1 2.22.0 2.21.5 2.21.4 2.21.3 2.21.2 2.21.1 2.21.0 2.20.0 2.19.0 2.18.1 2.18.0 2.17.0 2.16.0 2.15.1 2.15.0 2.14.2 2.14.1 2.14.0 2.13.19 2.13.18 2.13.17 2.13.16 2.13.15 2.13.13 2.13.12 2.13.11 2.13.10 2.13.9 2.13.8 2.13.7 2.13.6 2.13.5 2.13.4 2.13.3 2.13.2 2.13.1 2.13.0 2.12.1 2.12.0 2.11.0 2.10.0 2.9.0 2.8.1 2.8.0 2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.0 2.0.1 2.0.0 1.7.236 1.7.235 1.7.234 1.7.233 1.7.232 1.7.231 1.7.230 1.7.229 1.7.228 1.7.227 1.7.226 1.7.225 1.7.224 1.7.223 1.7.222 1.7.221 1.7.220 1.7.219 1.7.218 1.7.217 1.7.216 1.7.215 1.7.214 1.7.213 1.7.212 1.7.211 1.7.210 1.7.209 1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib phoenix_kit_web components featured_image.html.heex
Raw

lib/phoenix_kit_web/components/featured_image.html.heex

<div
  id={@id}
  role={if not blank?(assigns), do: "group"}
  aria-label={if not blank?(assigns), do: label_text(@label)}
  data-role={role(@id, "featured-image")}
  data-state={@display}
  class="flex w-fit flex-col items-start gap-1"
>
  <%!-- Thumbnail and menu share one row, the menu centred against the thumbnail;
       the error line below stays left-aligned with the root's items-start. --%>
  <div data-role={role(@id, "row")} class="flex items-center gap-1">
    <%= case @display do %>
      <% :empty -> %>
        <%= cond do %>
          <% @readonly -> %>
            <%!-- A viewer sees no placeholder, but still sees a host-supplied
                 one (an avatar's initials). --%>
            <.frame
              :if={@empty != []}
              id={@id}
              name="empty"
              size={@size}
              shape={@shape}
              tone={:solid}
            >
              {render_slot(@empty)}
            </.frame>
          <% choosable?(assigns) and @empty == [] -> %>
            <.frame
              as={:button}
              id={@id}
              name="open-picker"
              size={@size}
              shape={@shape}
              tone={:dashed}
              phx-click="open_picker"
              phx-target={@myself}
              aria-busy={to_string(@pending_scope)}
              title={gettext("Choose image")}
            >
              <span :if={@pending_scope} class="loading loading-spinner loading-sm"></span>
              <.icon :if={not @pending_scope} name="hero-photo" class={icon_class(@size)} />
              <span class={caption_class(@size)}>{gettext("Choose image")}</span>
            </.frame>
          <% choosable?(assigns) -> %>
            <.frame
              as={:button}
              id={@id}
              name="open-picker"
              size={@size}
              shape={@shape}
              tone={:solid}
              phx-click="open_picker"
              phx-target={@myself}
              aria-label={gettext("Choose image")}
              aria-busy={to_string(@pending_scope)}
              title={gettext("Choose image")}
            >
              {render_slot(@empty)}
            </.frame>
          <% @empty_hint != [] -> %>
            <div data-role={role(@id, "empty-hint")}>{render_slot(@empty_hint)}</div>
          <% @empty != [] -> %>
            <.frame id={@id} name="empty" size={@size} shape={@shape} tone={:solid}>
              {render_slot(@empty)}
            </.frame>
          <% true -> %>
            <.frame
              id={@id}
              name="empty"
              size={@size}
              shape={@shape}
              tone={:solid}
              role="img"
              aria-label={label_text(@label)}
            >
              <.icon name="hero-photo" class={icon_class(@size) <> " opacity-60"} />
            </.frame>
        <% end %>
      <% :ok -> %>
        <%= cond do %>
          <% @preview_click -> %>
            <.frame
              as={:button}
              id={@id}
              name="thumb"
              size={@size}
              shape={@shape}
              tone={:image}
              phx-click={@preview_click}
              aria-label={gettext("View")}
              title={gettext("View")}
              {@preview_values}
            >
              <.thumb_img id={@id} file={@file} size={@size} />
            </.frame>
          <% not @readonly and choosable?(assigns) -> %>
            <.frame
              as={:button}
              id={@id}
              name="thumb"
              size={@size}
              shape={@shape}
              tone={:image}
              phx-click="open_picker"
              phx-target={@myself}
              aria-label={gettext("Change image")}
              title={gettext("Change image")}
            >
              <.thumb_img id={@id} file={@file} size={@size} />
            </.frame>
          <% true -> %>
            <.frame id={@id} name="thumb" size={@size} shape={@shape} tone={:image}>
              <.thumb_img id={@id} file={@file} size={@size} />
            </.frame>
        <% end %>
      <% :processing -> %>
        <.frame
          id={@id}
          name="processing"
          size={@size}
          shape={@shape}
          tone={:solid}
          role="status"
          title={gettext("Processing image…")}
        >
          <span class="loading loading-spinner loading-md"></span>
          <span class="sr-only">{gettext("Processing image…")}</span>
        </.frame>
      <% :dangling -> %>
        <%= cond do %>
          <% @readonly and @empty == [] -> %>
            <%!-- A viewer cannot act on a missing image, so it gets what an
                 empty control gets: nothing, or the host's own placeholder. --%>
          <% @readonly -> %>
            <.frame id={@id} name="empty" size={@size} shape={@shape} tone={:solid}>
              {render_slot(@empty)}
            </.frame>
          <% @empty != [] -> %>
            <%!-- Avatars keep their initials; the missing image is only marked
                 (title + screen-reader text) and the menu still offers Remove. --%>
            <.frame
              id={@id}
              name="dangling"
              size={@size}
              shape={@shape}
              tone={:solid}
              title={gettext("Image missing")}
            >
              {render_slot(@empty)}
              <span class="sr-only">{gettext("Image missing")}</span>
            </.frame>
          <% true -> %>
            <.frame
              id={@id}
              name="dangling"
              size={@size}
              shape={@shape}
              tone={:warning}
              role="img"
              aria-label={gettext("Image missing")}
              title={gettext("Image missing")}
            >
              <.icon name="hero-exclamation-triangle" class={icon_class(@size)} />
              <span class={caption_class(@size)}>{gettext("Image missing")}</span>
            </.frame>
        <% end %>
    <% end %>

    <%!-- Always visible, beside the thumbnail rather than over it: a hover-only
         control is unreachable on a touch screen. --%>
    <div
      :if={not @readonly and @display != :empty}
      data-role={role(@id, "menu")}
      class="shrink-0"
    >
      <.table_row_menu
        id={"#{@id}-menu"}
        label={gettext("Image actions")}
        trigger_size={trigger_size(@size)}
      >
        <.table_row_menu_button
          :if={@display == :ok and @preview_click}
          icon="hero-eye"
          label={gettext("View")}
          data-role={role(@id, "view")}
          phx-click={@preview_click}
          {@preview_values}
        />
        <.table_row_menu_button
          :if={choosable?(assigns)}
          icon="hero-photo"
          label={gettext("Change image")}
          data-role={role(@id, "change")}
          phx-click="open_picker"
          phx-target={@myself}
        />
        <.table_row_menu_divider :if={
          choosable?(assigns) or (@display == :ok and @preview_click != nil)
        } />
        <.table_row_menu_button
          icon="hero-trash"
          label={gettext("Remove image")}
          variant="error"
          data-role={role(@id, "remove")}
          data-confirm={@confirm_remove}
          phx-click="clear"
          phx-target={@myself}
        />
      </.table_row_menu>
    </div>
  </div>

  <p
    :if={@error && not @readonly}
    role="alert"
    data-role={role(@id, "error")}
    class="max-w-xs text-xs text-error"
  >
    {@error}
  </p>

  <%!-- `MediaSelectorModal` carries a <form> and a file input of its own, and
       most hosts put this control inside their own <form>: nested forms are
       invalid, so the modal is teleported to <body>. Mounted only while open —
       it re-opens itself if it is passed `show` again after closing. --%>
  <.portal :if={@show_picker and not @readonly} id={"#{@id}-portal"} target="body">
    <.live_component
      module={MediaSelectorModal}
      id={"#{@id}-selector"}
      show={true}
      mode={:single}
      file_type_filter={:image}
      lock_file_type
      scope_folder_id={folder_uuid(@picker_scope)}
      selected_uuids={selected_uuids(assigns)}
      phoenix_kit_current_user={@phoenix_kit_current_user}
      title={label_text(@label)}
      notify={{__MODULE__, @id}}
    />
  </.portal>
</div>