sections/logos.ex
Logo cloud
A logo cloud of customer names set as plain wordmarks in a hairline grid, with a link to their stories.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.sections_logos animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
eyebrow |
string | "Customers" |
|
title |
string | "Used by 120 operations teams" |
Up to 44 characters. |
names |
list | ["Northwind", "Acme Tools", "Globex", "Initech", "Baltic Parts", "Oakline", ...] |
Up to twelve invented company names of up to 13 characters. Never a real brand. |
columns |
integer | 4 |
Cells per row, from two to four. |
link |
any | "Read their stories" |
The lime link under the grid. `nil` hides it. |
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/logos.ex
defmodule EHVisuals.Sections.Logos do
@moduledoc """
A logo cloud: customer names set as plain wordmarks in a grid of hairline
cells, under a short heading.
"""
use EHVisuals.Composition,
name: "sections/logos",
title: "Logo cloud",
description:
"A logo cloud of customer names set as plain wordmarks in a hairline grid, with a link to their stories."
@names [
"Northwind",
"Acme Tools",
"Globex",
"Initech",
"Baltic Parts",
"Oakline",
"Lumen Freight",
"Kestrel"
]
attr(:eyebrow, :string, default: "Customers")
attr(:title, :string, default: "Used by 120 operations teams", doc: "Up to 44 characters.")
attr(:names, :list,
default: @names,
doc: "Up to twelve invented company names of up to 13 characters. Never a real brand."
)
attr(:columns, :integer, default: 4, doc: "Cells per row, from two to four.")
attr(:link, :any,
default: "Read their stories",
doc: "The lime link under the grid. `nil` hides it."
)
attr(:label, :string, default: "A grid of eight customer names under a short heading")
frame_attrs()
@spec sections_logos(map()) :: Phoenix.LiveView.Rendered.t()
def sections_logos(assigns) do
columns = assigns.columns |> max(2) |> min(4)
names = Enum.take(assigns.names, 12)
rows = max(ceil(length(names) / columns), 1)
cell_w = 432 / columns
bottom = 78 + rows * 46
cells =
names
|> Enum.with_index()
|> Enum.map(fn {name, index} ->
style = Enum.at(~w(a b c d), rem(index, 4))
%{
text: if(style in ~w(b d), do: String.upcase(name), else: name),
style: style,
size: Map.fetch!(%{"a" => 14.5, "b" => 10, "c" => 14, "d" => 11}, style),
x: Float.round(24 + (rem(index, columns) + 0.5) * cell_w, 1),
y: 78 + div(index, columns) * 46,
index: min(index + 3, 16)
}
end)
assigns =
assigns
|> assign(:cells, cells)
|> assign(:rules, for(row <- 1..rows//1, row < rows, do: 78.5 + row * 46))
|> assign(
:posts,
for(col <- 1..columns//1, col < columns, do: Float.round(24.5 + col * cell_w, 1))
)
|> assign(:bottom, bottom)
|> assign(:link_w, if(assigns.link, do: String.length(assigns.link) * 5.7, else: 0))
|> assign(:height, bottom + if(assigns.link, do: 48, else: 24))
~H"""
<.frame name="sections/logos" 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 class="ehv-a-fade ehv-i-3">
<rect class="ehv-well" x="24.5" y="78.5" width="431" height={@bottom - 79} rx="10" />
<line :for={y <- @rules} class="ehv-line-faint" x1="25" x2="455" y1={y} y2={y} />
<line :for={x <- @posts} class="ehv-line-faint" x1={x} x2={x} y1="79" y2={@bottom - 1} />
</g>
<text
:for={cell <- @cells}
class={[
"ehv-sections-logos__name ehv-a-fade",
"ehv-sections-logos__name--#{cell.style}",
"ehv-i-#{cell.index}",
if(cell.style in ~w(b d), do: "ehv-t-3", else: "ehv-t-2")
]}
x={cell.x}
y={cell.y + 23 + cell.size * 0.34}
font-size={cell.size}
text-anchor="middle"
>
{cell.text}
</text>
<g :if={@link} class="ehv-a-fade ehv-i-12">
<text
class="ehv-t-strong ehv-t-lime"
x={240 - 8}
y={@bottom + 28}
font-size="11"
text-anchor="middle"
>
{@link}
</text>
<path
class="ehv-line-lime ehv-round ehv-sections-logos__arrow"
d={"M#{240 - 8 + @link_w / 2 + 6} #{@bottom + 24} h7 m-2.8 -2.8 l2.8 2.8 l-2.8 2.8"}
/>
</g>
</g>
</svg>
</.frame>
"""
end
end
assets/css/sections/logos.css
/* sections/logos */
.ehv-sections-logos { --ehv-max-w: 520px; --ehv-iso-scale: 0.82; }
/* Four ways to set a name, so the grid reads as different companies. */
.ehv svg .ehv-sections-logos__name--a { font-weight: 500; letter-spacing: -0.035em; }
.ehv svg .ehv-sections-logos__name--b { font-family: var(--ehv-mono); letter-spacing: 0.16em; }
.ehv svg .ehv-sections-logos__name--c { letter-spacing: 0.01em; }
.ehv svg .ehv-sections-logos__name--d { font-weight: 500; letter-spacing: 0.12em; }
.ehv--hover svg .ehv-sections-logos__name { transition: fill 200ms var(--ehv-ease); }
.ehv--hover:hover svg .ehv-sections-logos__name { fill: var(--ehv-ink); }
.ehv--hover .ehv-sections-logos__arrow { transition: transform 200ms var(--ehv-ease); }
.ehv--hover:hover .ehv-sections-logos__arrow { transform: translateX(2px); }