Packages
phoenix_kit
2.37.2
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
Current section
Files
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>