empowered.house ↗

sections/team.ex

Team

A team section with member cards, each an avatar of initials, a name and a role, and one member picked out.

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

<div class="h-96 rounded-xl border border-border">
  <.sections_team animated />
</div>
default
isometric
three members · custom copy
hover · no glow · two rows
light surface
isometric · fade out · still

Props

Prop Type Default What it does
eyebrow string "Team"
title string "The people you will work with" Up to 44 characters.
members list [%{name: "Marta Nowak", role: "Operations lead"}, %{name: "Jan Kowalski", role: "Finance lead"}, ...] Up to eight `%{name, role}`, four to a row. A name of up to 16 characters, a role of 17.
highlight integer 0 Index of the member picked out with a lime ring, your contact. Negative for none.
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/sections/team.ex
team.ex
defmodule EHVisuals.Sections.Team do
  @moduledoc """
  A team section: member cards with an avatar of initials, a name and a role,
  and one member picked out with a lime ring.
  """

  use EHVisuals.Composition,
    name: "sections/team",
    title: "Team",
    description:
      "A team section with member cards, each an avatar of initials, a name and a role, and one member picked out."

  @members [
    %{name: "Marta Nowak", role: "Operations lead"},
    %{name: "Jan Kowalski", role: "Finance lead"},
    %{name: "Ewa Lis", role: "Process engineer"},
    %{name: "Piotr Zielinski", role: "Agent engineer"}
  ]

  attr(:eyebrow, :string, default: "Team")
  attr(:title, :string, default: "The people you will work with", doc: "Up to 44 characters.")

  attr(:members, :list,
    default: @members,
    doc:
      "Up to eight `%{name, role}`, four to a row. A name of up to 16 characters, a role of 17."
  )

  attr(:highlight, :integer,
    default: 0,
    doc: "Index of the member picked out with a lime ring, your contact. Negative for none."
  )

  attr(:label, :string, default: "Four team members, each with initials, a name and a role")
  frame_attrs()

  @spec sections_team(map()) :: Phoenix.LiveView.Rendered.t()
  def sections_team(assigns) do
    members = Enum.take(assigns.members, 8)
    columns = members |> length() |> min(4) |> max(1)
    rows = max(ceil(length(members) / columns), 1)
    width = Float.round((432 - (columns - 1) * 10) / columns, 1)

    cards =
      members
      |> Enum.with_index()
      |> Enum.map(fn {member, index} ->
        x = Float.round(24 + rem(index, columns) * (width + 10), 1)

        %{
          x: x,
          cx: Float.round(x + width / 2, 1),
          y: 76 + div(index, columns) * 118,
          name: member.name,
          role: member.role,
          initials: initials(member.name),
          on: index == assigns.highlight,
          index: index
        }
      end)

    assigns =
      assigns
      |> assign(:cards, cards)
      |> assign(:width, width)
      |> assign(:height, 76 + rows * 118 - 10 + 24)

    ~H"""
    <.frame name="sections/team" label={@label} {frame_opts(assigns)}>
      <svg viewBox={"0 0 480 #{@height}"}>
        <g class="ehv-h-lift">
          <rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="479" height={@height - 1} rx="12" />
          <text
            class="ehv-mono ehv-a-fade ehv-i-1"
            x="240"
            y="34"
            font-size="8.5"
            text-anchor="middle"
          >
            {String.upcase(@eyebrow)}
          </text>
          <text
            class="ehv-t-strong ehv-a-fade ehv-i-2"
            x="240"
            y="58"
            font-size="16"
            text-anchor="middle"
          >
            {@title}
          </text>
          <g :for={card <- @cards} class={"ehv-a-rise ehv-i-#{card.index + 3}"}>
            <rect
              class="ehv-card-2 ehv-sections-team__card"
              x={card.x + 0.5}
              y={card.y + 0.5}
              width={@width - 1}
              height="107"
              rx="10"
            />
            <circle
              class={["ehv-card-3", card.on && "ehv-sections-team__avatar--on"]}
              cx={card.cx}
              cy={card.y + 36}
              r="18"
            />
            <text
              class={["ehv-mono", if(card.on, do: "ehv-t-ink", else: "ehv-t-2")]}
              x={card.cx}
              y={card.y + 39.3}
              font-size="9.5"
              text-anchor="middle"
            >
              {card.initials}
            </text>
            <g :if={card.on}>
              <circle class="ehv-fill-surface-2" cx={card.cx + 13.5} cy={card.y + 49} r="6" />
              <circle
                class="ehv-sections-team__dot ehv-a-pulse"
                cx={card.cx + 13.5}
                cy={card.y + 49}
                r="3.5"
              />
            </g>
            <text
              class="ehv-t-strong"
              x={card.cx}
              y={card.y + 76}
              font-size="11.5"
              text-anchor="middle"
            >
              {card.name}
            </text>
            <text class="ehv-mono" x={card.cx} y={card.y + 91} font-size="7.5" text-anchor="middle">
              {String.upcase(card.role)}
            </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/sections/team.css
team.css
/* sections/team */
.ehv-sections-team { --ehv-max-w: 520px; --ehv-iso-scale: 0.82; }

.ehv .ehv-sections-team__avatar--on { stroke: var(--ehv-lime); stroke-width: 1.25; }
/* The line colour of lime, so the dot keeps its contrast on the light surface. */
.ehv .ehv-sections-team__dot { fill: var(--ehv-lime); }

.ehv--hover .ehv-sections-team__card { transition: stroke 200ms var(--ehv-ease); }
.ehv--hover .ehv-sections-team__card:hover { stroke: var(--ehv-border-strong); }