avatars/profile-card.ex
Profile card
A compact profile card with an avatar, name, role and three key numbers.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.avatars_profile_card animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
name |
string | "Marta Nowak" |
Up to 18 characters. |
role |
string | "Head of operations" |
Up to 30 characters. |
status |
any | "On shift" |
The lime pill on the right, up to 10 characters. `nil` hides it. |
stats |
list | [%{label: "Approvals", value: "128"}, %{label: "Avg. reply", value: "4.2 h"}, ...] |
Two to four `%{value, label}`. Values up to 6 characters, labels up to 12. |
Frame props, shared by every composition
| Prop | Type | Default | What it does |
|---|---|---|---|
animated |
boolean | false |
Turns motion on. Without it the composition renders in its final, still state. |
trigger |
"mount" | "in-view" | "in-view-repeat" | "in-view" |
When the entrance plays: on page load, once when scrolled into view, or every time it comes back into view. |
hover |
boolean | false |
Enables the hover micro-interactions the composition supports. |
glow |
boolean | true |
The warm incandescent halo behind the composition. |
fade_out |
boolean | false |
Fades the bottom edge into the surface underneath. |
isometric |
boolean | false |
Tilts the composition into the isometric plane of the platform stack. |
theme |
"dark" | "light" | "dark" |
`dark` for the site canvas, `light` for the cream reversed surface. |
class |
string | nil |
Extra classes for the frame. |
rest |
global | — |
Any other HTML attribute, set on the frame. |
Source
Two files. Copy them into your app if you would rather own the code than depend on the library.
lib/eh_visuals/avatars/profile-card.ex
defmodule EHVisuals.Avatars.ProfileCard do
@moduledoc """
A compact profile card: avatar, name, role, status and a few key numbers.
"""
use EHVisuals.Composition,
name: "avatars/profile-card",
title: "Profile card",
description: "A compact profile card with an avatar, name, role and three key numbers."
@stats [
%{value: "128", label: "Approvals"},
%{value: "4.2 h", label: "Avg. reply"},
%{value: "96%", label: "On time"}
]
attr(:name, :string, default: "Marta Nowak", doc: "Up to 18 characters.")
attr(:role, :string, default: "Head of operations", doc: "Up to 30 characters.")
attr(:status, :any,
default: "On shift",
doc: "The lime pill on the right, up to 10 characters. `nil` hides it."
)
attr(:stats, :list,
default: @stats,
doc: "Two to four `%{value, label}`. Values up to 6 characters, labels up to 12."
)
attr(:label, :string,
default: "A profile card for Marta Nowak, head of operations, with three numbers"
)
frame_attrs()
@spec avatars_profile_card(map()) :: Phoenix.LiveView.Rendered.t()
def avatars_profile_card(assigns) do
stats = Enum.take(assigns.stats, 4)
column = 280 / max(length(stats), 1)
items =
for {stat, index} <- Enum.with_index(stats) do
%{
value: stat.value,
label: stat.label,
index: index,
x: Float.round(20 + index * column, 1)
}
end
assigns =
assigns
|> assign(:items, items)
|> assign(:initials, initials(assigns.name))
|> assign(
:pill_w,
if(assigns.status, do: 26 + String.length(assigns.status) * 5.8, else: 0)
)
~H"""
<.frame name="avatars/profile-card" label={@label} {frame_opts(assigns)}>
<svg viewBox="0 0 320 158">
<g class="ehv-h-lift">
<rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="319" height="157" rx="12" />
<g class="ehv-a-pop ehv-i-1">
<circle class="ehv-card-3 ehv-avatars-profile-card__avatar" cx="44" cy="46" r="24" />
<text class="ehv-mono ehv-t-2" x="44" y="49.8" font-size="10.5" text-anchor="middle">
{@initials}
</text>
</g>
<g class="ehv-a-fade ehv-i-2">
<text class="ehv-t-strong" x="80" y="43" font-size="15">{@name}</text>
<text class="ehv-mono" x="80" y="59" font-size="8.5">{String.upcase(@role)}</text>
</g>
<g :if={@status} class="ehv-a-fade ehv-i-3">
<rect
class="ehv-fill-lime-soft"
x={300 - @pill_w}
y="24"
width={@pill_w}
height="20"
rx="6"
/>
<circle class="ehv-fill-lime ehv-a-pulse" cx={300 - @pill_w + 11} cy="34" r="2.5" />
<text class="ehv-mono ehv-t-lime" x="291" y="37.5" font-size="8.5" text-anchor="end">
{String.upcase(@status)}
</text>
</g>
<line class="ehv-line-faint" x1="20" x2="300" y1="84.5" y2="84.5" />
<g :for={item <- @items} class={"ehv-a-rise ehv-i-#{item.index + 4}"}>
<line
:if={item.index > 0}
class="ehv-line-faint"
x1={item.x - 12}
x2={item.x - 12}
y1="100"
y2="138"
/>
<text class="ehv-t-display" x={item.x} y="119" font-size="20">{item.value}</text>
<text class="ehv-mono" x={item.x} y="136" font-size="8">{String.upcase(item.label)}</text>
</g>
</g>
</svg>
</.frame>
"""
end
defp initials(name) do
name |> String.split() |> Enum.take(2) |> Enum.map_join(&String.first/1) |> String.upcase()
end
end
assets/css/avatars/profile-card.css
/* avatars/profile-card */
.ehv-avatars-profile-card { --ehv-max-w: 350px; }
.ehv--hover .ehv-avatars-profile-card__avatar { transition: stroke 200ms var(--ehv-ease); }
.ehv--hover:hover .ehv-avatars-profile-card__avatar { stroke: var(--ehv-lime); }