empowered.house ↗

avatars/profile-card.ex

Profile card

A compact profile card with an avatar, name, role and three key numbers.

Use it
# in the module that renders the template
use EHVisuals

<div class="h-96 rounded-xl border border-border">
  <.avatars_profile_card animated />
</div>
default
isometric
an agent · custom copy
two numbers · no status · hover · no glow
light surface
isometric · fade out · still

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
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
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); }