empowered.house ↗

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.

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

<div class="h-96 rounded-xl border border-border">
  <.sections_logos animated />
</div>
default
isometric
six names · three per row
hover · no glow · no link
light surface
isometric · fade out · still

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