sections/backgrounds.ex
Section backgrounds
A section preview with one of the brand's backgrounds behind a heading: grid, dots, warm glow or diagonal hairlines.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.sections_backgrounds animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
pattern |
"grid" | "dots" | "glow" | "lines" | "grid" |
The background behind the heading. |
eyebrow |
string | "Order to cash" |
|
heading |
string | "One place for every order" |
34 characters at most. |
text |
string | "Orders, stock and invoices in the same system." |
62 characters at most. Empty hides it. |
action |
string | "See how it works" |
|
secondary |
string | "Read the docs" |
Empty hides the second button. |
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/backgrounds.ex
defmodule EHVisuals.Sections.Backgrounds do
@moduledoc """
A section preview that shows one of the brand's background styles behind a
heading: a faint grid, a dot field, the warm glow, or diagonal hairlines.
Every style is drawn from plain shapes. The glow is a stack of flat
ellipses, so the picture needs no gradient and no id.
"""
use EHVisuals.Composition,
name: "sections/backgrounds",
title: "Section backgrounds",
description:
"A section preview with one of the brand's backgrounds behind a heading: grid, dots, warm glow or diagonal hairlines."
@labels %{
"grid" => "A section heading over a faint grid",
"dots" => "A section heading over a field of dots",
"glow" => "A section heading over the warm glow",
"lines" => "A section heading over diagonal hairlines"
}
attr(:pattern, :string,
default: "grid",
values: ~w(grid dots glow lines),
doc: "The background behind the heading."
)
attr(:eyebrow, :string, default: "Order to cash")
attr(:heading, :string, default: "One place for every order", doc: "34 characters at most.")
attr(:text, :string,
default: "Orders, stock and invoices in the same system.",
doc: "62 characters at most. Empty hides it."
)
attr(:action, :string, default: "See how it works")
attr(:secondary, :string, default: "Read the docs", doc: "Empty hides the second button.")
attr(:label, :any,
default: nil,
doc: "What the picture shows. Left empty, it is written from the pattern."
)
frame_attrs()
@spec sections_backgrounds(map()) :: Phoenix.LiveView.Rendered.t()
def sections_backgrounds(assigns) do
action_w = Float.round(26 + String.length(assigns.action) * 6.0, 1)
secondary_w =
if assigns.secondary == "",
do: 0,
else: Float.round(26 + String.length(assigns.secondary) * 6.0, 1)
total = action_w + if(secondary_w > 0, do: 10 + secondary_w, else: 0)
buttons_y = if assigns.text == "", do: 142, else: 164
assigns =
assigns
|> assign(:label, assigns.label || Map.fetch!(@labels, assigns.pattern))
|> assign(:columns, for(index <- 0..10, do: {20 + index * 40, index}))
|> assign(:lanes, for(index <- 0..6, do: {12 + index * 40, index}))
|> assign(:dot_columns, for(index <- 0..18, do: {22 + index * 22, index}))
|> assign(:dot_rows, for(index <- 0..10, do: 22 + index * 22))
|> assign(:rings, rings())
|> assign(:clearing, for(index <- 0..5, do: {190 - index * 22, 84 - index * 9}))
|> assign(:hairlines, hairlines())
|> assign(:action_w, action_w)
|> assign(:secondary_w, secondary_w)
|> assign(:action_x, Float.round(220 - total / 2, 1))
|> assign(:secondary_x, Float.round(220 - total / 2 + action_w + 10, 1))
|> assign(:buttons_y, buttons_y)
~H"""
<.frame name="sections/backgrounds" label={@label} {frame_opts(assigns)}>
<svg viewBox="0 0 440 264">
<g class="ehv-h-lift">
<rect class="ehv-well ehv-a-fade" x="0.5" y="0.5" width="439" height="263" rx="12" />
<g :if={@pattern == "grid"}>
<line
:for={{x, index} <- @columns}
class={"ehv-line-soft ehv-a-draw ehv-i-#{index + 1}"}
x1={x + 0.5}
x2={x + 0.5}
y1="1"
y2="263"
pathLength="1"
/>
<line
:for={{y, index} <- @lanes}
class={"ehv-line-soft ehv-a-draw ehv-i-#{index + 4}"}
x1="1"
x2="439"
y1={y + 0.5}
y2={y + 0.5}
pathLength="1"
/>
</g>
<g :if={@pattern == "dots"}>
<g :for={{x, index} <- @dot_columns} class={"ehv-a-fade ehv-i-#{index + 1}"}>
<circle :for={y <- @dot_rows} class="ehv-fill-strong" cx={x} cy={y} r="1.2" />
</g>
</g>
<g :if={@pattern == "glow"}>
<ellipse
:for={{{rx, ry}, index} <- @rings}
class={"ehv-sections-backgrounds__glow ehv-a-fade ehv-i-#{div(index, 2) + 1}"}
cx="220"
cy="132"
rx={rx}
ry={ry}
/>
</g>
<g :if={@pattern == "lines"}>
<line
:for={{{x1, y1, x2, y2}, index} <- @hairlines}
class={"ehv-line-soft ehv-a-draw ehv-i-#{div(index, 2) + 1}"}
x1={x1}
y1={y1}
x2={x2}
y2={y2}
pathLength="1"
/>
</g>
<g :if={@pattern != "glow"} class="ehv-a-fade ehv-i-5">
<ellipse
:for={{rx, ry} <- @clearing}
class="ehv-sections-backgrounds__clearing"
cx="220"
cy="128"
rx={rx}
ry={ry}
/>
</g>
<g class="ehv-a-rise ehv-i-6">
<text class="ehv-mono ehv-t-lime" x="220" y="86" font-size="8.5" text-anchor="middle">
{String.upcase(@eyebrow)}
</text>
<text class="ehv-t-display" x="220" y="118" font-size="24" text-anchor="middle">
{@heading}
</text>
<text
:if={@text != ""}
class="ehv-t-2"
x="220"
y="142"
font-size="12"
text-anchor="middle"
>
{@text}
</text>
</g>
<g class="ehv-a-rise ehv-i-8">
<rect
class="ehv-fill-ink ehv-sections-backgrounds__action"
x={@action_x}
y={@buttons_y}
width={@action_w}
height="28"
rx="8"
/>
<text
class="ehv-t-strong ehv-sections-backgrounds__action-text"
x={@action_x + @action_w / 2}
y={@buttons_y + 18}
font-size="11"
text-anchor="middle"
>
{@action}
</text>
<g :if={@secondary != ""}>
<rect
class="ehv-card"
x={@secondary_x + 0.5}
y={@buttons_y + 0.5}
width={@secondary_w - 1}
height="27"
rx="8"
/>
<text
class="ehv-t-strong"
x={@secondary_x + @secondary_w / 2}
y={@buttons_y + 18}
font-size="11"
text-anchor="middle"
>
{@secondary}
</text>
</g>
</g>
<text class="ehv-mono ehv-a-fade ehv-i-10" x="16" y="248" font-size="7.5">
BACKGROUND / {String.upcase(@pattern)}
</text>
</g>
</svg>
</.frame>
"""
end
# The glow: flat ellipses that get smaller, each a little more light.
defp rings do
for index <- 0..13 do
{{206 - index * 13, 118 - index * 7.5}, index}
end
end
# Hairlines running up to the right, each cut where it meets the edge of
# the section. The box is inset so no line pokes out of a rounded corner.
defp hairlines do
{left, top, right, bottom} = {3, 3, 437, 261}
for {sum, index} <- Enum.with_index(Enum.to_list(28..676//24)) do
{x1, y1} = if sum - top <= right, do: {sum - top, top}, else: {right, sum - right}
{x2, y2} = if sum - bottom >= left, do: {sum - bottom, bottom}, else: {left, sum - left}
{{x1, y1, x2, y2}, index}
end
end
end
assets/css/sections/backgrounds.css
/* sections/backgrounds */
.ehv-sections-backgrounds { --ehv-max-w: 500px; --ehv-step: 60ms; --ehv-iso-scale: 0.82; }
/* One step of the warm glow. Fourteen of them stack up towards the middle. */
.ehv-sections-backgrounds__glow { fill: var(--ehv-glow); opacity: 0.065; }
/* The pattern thins out behind the heading: flat ellipses in the colour of
the section, stacked the same way. */
.ehv-sections-backgrounds__clearing { fill: var(--ehv-canvas); opacity: 0.22; }
/* The first button is ink on the section, with the text reversed. */
.ehv svg .ehv-sections-backgrounds__action-text { fill: var(--ehv-canvas); }
.ehv--hover .ehv-sections-backgrounds__action { transition: fill 200ms var(--ehv-ease); }
.ehv--hover .ehv-sections-backgrounds__action:hover { fill: var(--ehv-lime-fill); }