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.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.sections_team animated />
</div>
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
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
/* 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); }