empowered.house ↗

sections/features.ex

Features

A features section with a three-by-two grid of small drawn icons, each with a title and a line of description.

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

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

Props

Prop Type Default What it does
eyebrow string "Platform"
heading string "What every process gets"
features list [%{title: "Workflows", icon: "flow"}, %{title: "Agents", icon: "agent"}, ...] Up to six `%{icon, title}`, three to a row. Icon is `flow`, `agent`, `check`, `sync`, `chart`, `log`, `lock` or `doc`. A title is up to about 16 characters.
highlight integer 1 Index of the feature whose icon is lime. A negative index marks 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/features.ex
features.ex
defmodule EHVisuals.Sections.Features do
  @moduledoc """
  A features section: a grid of small drawn icons, each with a title and a
  line of description.
  """

  use EHVisuals.Composition,
    name: "sections/features",
    title: "Features",
    description:
      "A features section with a three-by-two grid of small drawn icons, each with a title and a line of description."

  @features [
    %{icon: "flow", title: "Workflows"},
    %{icon: "agent", title: "Agents"},
    %{icon: "check", title: "Approvals"},
    %{icon: "sync", title: "ERP sync"},
    %{icon: "chart", title: "Reporting"},
    %{icon: "log", title: "Audit trail"}
  ]

  # Icons on a 16 by 16 grid, drawn with a 1.25 stroke.
  @icons %{
    "flow" => "M1.5 3h4.5v4h-4.5z M10 9h4.5v4h-4.5z M6 5h2.5v6h1.5",
    "agent" => "M3 5.5h10v7.5h-10z M8 5.5v-3 M6 9v1 M10 9v1",
    "check" => "M8 1.5a6.5 6.5 0 1 0 0 13a6.5 6.5 0 1 0 0 -13z M5.2 8.2l2 2l3.8 -4.2",
    "sync" => "M2 5.5h11.5 m-3 -3 l3 3 l-3 3 M14 10.5h-11.5 m3 -3 l-3 3 l3 3",
    "chart" => "M2 14h12 M4.5 14v-4 M8 14v-7.5 M11.5 14v-11",
    "log" => "M2.5 4h1 M6 4h7.5 M2.5 8h1 M6 8h7.5 M2.5 12h1 M6 12h5",
    "lock" => "M3.5 7.5h9v6h-9z M5.5 7.5v-2a2.5 2.5 0 0 1 5 0v2",
    "doc" => "M4 1.5h5.5l3 3v10h-8.5z M9.5 1.5v3h3 M6 8.5h4.5 M6 11h4.5"
  }

  @dot "M8 5a3 3 0 1 0 0 6a3 3 0 1 0 0 -6z"

  # Widths of the two description lines, cycled by position.
  @lines [{104, 66}, {96, 78}, {108, 52}, {100, 70}, {92, 60}, {106, 74}]

  attr(:eyebrow, :string, default: "Platform")
  attr(:heading, :string, default: "What every process gets")

  attr(:features, :list,
    default: @features,
    doc:
      "Up to six `%{icon, title}`, three to a row. Icon is `flow`, `agent`, `check`, `sync`, `chart`, `log`, `lock` or `doc`. A title is up to about 16 characters."
  )

  attr(:highlight, :integer,
    default: 1,
    doc: "Index of the feature whose icon is lime. A negative index marks none."
  )

  attr(:label, :string,
    default: "A grid of six features, each with an icon, a title and a line of description"
  )

  frame_attrs()

  @spec sections_features(map()) :: Phoenix.LiveView.Rendered.t()
  def sections_features(assigns) do
    cells =
      assigns.features
      |> Enum.take(6)
      |> Enum.with_index()
      |> Enum.map(fn {feature, index} ->
        %{
          title: feature.title,
          icon: Map.get(@icons, Map.get(feature, :icon), @dot),
          index: index,
          x: 24 + rem(index, 3) * 121,
          y: 80 + div(index, 3) * 92,
          lines:
            @lines |> Enum.at(rem(index, length(@lines))) |> Tuple.to_list() |> Enum.with_index()
        }
      end)

    rows = max(div(length(cells) + 2, 3), 1)

    assigns =
      assigns
      |> assign(:cells, cells)
      |> assign(:rules, Enum.map(1..rows//1, &(80 + &1 * 92 - 10)) |> Enum.drop(-1))
      |> assign(:height, 80 + rows * 72 + (rows - 1) * 20 + 24)

    ~H"""
    <.frame name="sections/features" label={@label} {frame_opts(assigns)}>
      <svg viewBox={"0 0 400 #{@height}"}>
        <g class="ehv-h-lift">
          <rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="399" height={@height - 1} rx="12" />
          <text class="ehv-mono ehv-a-fade ehv-i-1" x="24" y="34" font-size="8.5">
            {String.upcase(@eyebrow)}
          </text>
          <text class="ehv-t-strong ehv-a-fade ehv-i-2" x="24" y="58" font-size="16">{@heading}</text>
          <line
            :for={y <- @rules}
            class="ehv-line-faint"
            x1="24"
            x2="376"
            y1={y + 0.5}
            y2={y + 0.5}
          />
          <g
            :for={cell <- @cells}
            class={"ehv-sections-features__cell ehv-a-rise ehv-i-#{cell.index + 3}"}
          >
            <rect
              class={[
                "ehv-sections-features__tile",
                if(cell.index == @highlight, do: "ehv-fill-lime-soft", else: "ehv-card-2")
              ]}
              x={cell.x + 0.5}
              y={cell.y + 0.5}
              width="27"
              height="27"
              rx="8"
            />
            <g transform={"translate(#{cell.x + 6} #{cell.y + 6})"}>
              <path
                class={[
                  "ehv-round",
                  if(cell.index == @highlight, do: "ehv-line-lime", else: "ehv-line-ink")
                ]}
                d={cell.icon}
              />
            </g>
            <text class="ehv-t-strong" x={cell.x} y={cell.y + 47} font-size="12">{cell.title}</text>
            <rect
              :for={{width, line} <- cell.lines}
              class={"ehv-fill-soft ehv-a-grow-x ehv-i-#{cell.index + 4 + line}"}
              x={cell.x}
              y={cell.y + 56 + line * 11}
              width={width}
              height="6"
              rx="3"
            />
          </g>
        </g>
      </svg>
    </.frame>
    """
  end
end
assets/css/sections/features.css
features.css
/* sections/features */
.ehv-sections-features { --ehv-max-w: 440px; }

.ehv-sections-features__tile { stroke-width: 1; }

.ehv--hover .ehv-sections-features__tile { transition: stroke 200ms var(--ehv-ease); }
.ehv--hover .ehv-sections-features__cell:hover .ehv-sections-features__tile {
  stroke: var(--ehv-lime);
}