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