empowered.house ↗

avatars/grid.ex

Avatar grid

A team as a grid of initials, revealed one by one, with one person marked.

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

<div class="h-96 rounded-xl border border-border">
  <.avatars_grid animated />
</div>
default
isometric
four columns · custom copy
nobody marked · hover · no glow
light surface
isometric · fade out · still

Props

Prop Type Default What it does
eyebrow string "Operations team"
people list ["Marta Nowak", "Jan Kowalski", "Ewa Lis", "Piotr Zielinski", "Anna Maj", "Tomasz Wrona", ...] Up to 24 names. Each shows as its initials.
columns integer 5 Avatars per row, three to six.
highlight any 7 Index of the person marked in lime, from 0. `nil` marks nobody and drops the footer.
note string "On call today" What the marked person is doing, next to the name. Up to 22 characters.
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/grid.ex
grid.ex
defmodule EHVisuals.Avatars.Grid do
  @moduledoc """
  A team as a grid of initials, revealed one by one, with one person marked.
  """

  use EHVisuals.Composition,
    name: "avatars/grid",
    title: "Avatar grid",
    description: "A team as a grid of initials, revealed one by one, with one person marked."

  @people [
    "Marta Nowak",
    "Jan Kowalski",
    "Ewa Lis",
    "Piotr Zielinski",
    "Anna Maj",
    "Tomasz Wrona",
    "Olga Sowa",
    "Adam Krol",
    "Kasia Duda",
    "Marek Bak",
    "Iga Stec",
    "Pawel Rak",
    "Zofia Kot",
    "Leon Wilk",
    "Nina Gil"
  ]

  attr(:eyebrow, :string, default: "Operations team")
  attr(:people, :list, default: @people, doc: "Up to 24 names. Each shows as its initials.")
  attr(:columns, :integer, default: 5, doc: "Avatars per row, three to six.")

  attr(:highlight, :any,
    default: 7,
    doc: "Index of the person marked in lime, from 0. `nil` marks nobody and drops the footer."
  )

  attr(:note, :string,
    default: "On call today",
    doc: "What the marked person is doing, next to the name. Up to 22 characters."
  )

  attr(:label, :string, default: "A team of fifteen people as initials, one of them on call")
  frame_attrs()

  @spec avatars_grid(map()) :: Phoenix.LiveView.Rendered.t()
  def avatars_grid(assigns) do
    columns = assigns.columns |> max(3) |> min(6)
    people = Enum.take(assigns.people, 24)
    cell = 300 / columns
    radius = Float.round(min(cell / 2 - 6, 26.0), 1)
    rows = max(ceil(length(people) / columns), 1)
    bottom = 48 + rows * cell

    avatars =
      for {name, index} <- Enum.with_index(people) do
        row = div(index, columns)
        column = rem(index, columns)

        %{
          initials: initials(name),
          on: index == assigns.highlight,
          cx: Float.round(20 + column * cell + cell / 2, 1),
          cy: Float.round(48 + row * cell + cell / 2, 1),
          stagger: min(row + column + 2, 24)
        }
      end

    marked = is_integer(assigns.highlight) && Enum.at(people, assigns.highlight)

    assigns =
      assigns
      |> assign(:avatars, avatars)
      |> assign(:radius, radius)
      |> assign(:font, Float.round(radius * 0.4, 1))
      |> assign(:count, if(length(people) == 1, do: "1 person", else: "#{length(people)} people"))
      |> assign(:marked, marked)
      |> assign(:bottom, Float.round(bottom, 1))
      |> assign(:height, Float.round(if(marked, do: bottom + 48, else: bottom + 14), 1))

    ~H"""
    <.frame name="avatars/grid" label={@label} {frame_opts(assigns)}>
      <svg viewBox={"0 0 340 #{@height}"}>
        <g class="ehv-h-lift">
          <rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="339" height={@height - 1} rx="12" />
          <g class="ehv-a-fade ehv-i-1">
            <text class="ehv-mono" x="20" y="31" font-size="9">{String.upcase(@eyebrow)}</text>
            <text class="ehv-mono ehv-t-4" x="320" y="31" font-size="9" text-anchor="end">
              {String.upcase(@count)}
            </text>
          </g>
          <g :for={avatar <- @avatars} class={"ehv-a-pop ehv-i-#{avatar.stagger}"}>
            <g :if={avatar.on}>
              <circle class="ehv-fill-lime ehv-a-ping" cx={avatar.cx} cy={avatar.cy} r={@radius} />
              <circle class="ehv-line-lime" cx={avatar.cx} cy={avatar.cy} r={@radius + 4} />
              <circle class="ehv-fill-lime" cx={avatar.cx} cy={avatar.cy} r={@radius} />
            </g>
            <circle
              :if={!avatar.on}
              class="ehv-card-3 ehv-avatars-grid__avatar"
              cx={avatar.cx}
              cy={avatar.cy}
              r={@radius}
            />
            <text
              class={["ehv-mono", if(avatar.on, do: "ehv-t-on-lime", else: "ehv-t-2")]}
              x={avatar.cx}
              y={avatar.cy + @font * 0.36}
              font-size={@font}
              text-anchor="middle"
            >
              {avatar.initials}
            </text>
          </g>
          <g :if={@marked} class="ehv-a-fade ehv-i-10">
            <line class="ehv-line-faint" x1="20" x2="320" y1={@bottom + 6.5} y2={@bottom + 6.5} />
            <circle class="ehv-fill-lime" cx="24" cy={@bottom + 25} r="3" />
            <text x="36" y={@bottom + 29.5} font-size="12.5">{@marked}</text>
            <text
              class="ehv-mono ehv-t-lime"
              x="320"
              y={@bottom + 28.5}
              font-size="8.5"
              text-anchor="end"
            >
              {String.upcase(@note)}
            </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/grid.css
grid.css
/* avatars/grid */
.ehv-avatars-grid { --ehv-max-w: 370px; --ehv-step: 70ms; }

.ehv--hover .ehv-avatars-grid__avatar { transition: stroke 200ms var(--ehv-ease); }
.ehv--hover .ehv-avatars-grid__avatar:hover { stroke: var(--ehv-lime); }