sections/bento.ex
Bento section
A bento grid section in miniature: one featured tile and mixed-size cards with abstract contents.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.sections_bento animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
eyebrow |
string | "Today" |
|
title |
string | "Orders handled" |
The featured tile. 30 characters at most. |
value |
string | "1,284" |
Nine characters at most. |
values |
list | [12, 14, 13, 17, 16, 21, ...] |
The series behind the number, oldest first. |
cards |
list | [%{title: "Approvals", note: "3 waiting"}, %{title: "Stock", note: "72% counted"}, ...] |
Two to four `%{title, note}`. They draw people, a ring, rows and steps, in that order. Titles of 18 characters at most. |
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/bento.ex
defmodule EHVisuals.Sections.Bento do
@moduledoc """
A bento grid section in miniature: one featured tile with a number and its
trend, and smaller cards of mixed sizes with abstract contents.
"""
use EHVisuals.Composition,
name: "sections/bento",
title: "Bento section",
description:
"A bento grid section in miniature: one featured tile and mixed-size cards with abstract contents."
@cards [
%{title: "Approvals", note: "3 waiting"},
%{title: "Stock", note: "72% counted"},
%{title: "Agents", note: "4 running"},
%{title: "Month-end close", note: "Day 3 of 5"}
]
# What each card draws, in order.
@kinds ~w(people ring rows steps)a
attr(:eyebrow, :string, default: "Today")
attr(:title, :string,
default: "Orders handled",
doc: "The featured tile. 30 characters at most."
)
attr(:value, :string, default: "1,284", doc: "Nine characters at most.")
attr(:values, :list,
default: [12, 14, 13, 17, 16, 21, 19, 24, 23, 28, 27, 33],
doc: "The series behind the number, oldest first."
)
attr(:cards, :list,
default: @cards,
doc:
"Two to four `%{title, note}`. They draw people, a ring, rows and steps, in that order. Titles of 18 characters at most."
)
attr(:label, :string,
default: "A bento grid with a featured orders tile and four smaller cards"
)
frame_attrs()
@spec sections_bento(map()) :: Phoenix.LiveView.Rendered.t()
def sections_bento(assigns) do
cards = Enum.take(assigns.cards, 4)
tall = length(cards) > 2
tiles =
cards
|> Enum.zip(slots(length(cards)))
|> Enum.zip(@kinds)
|> Enum.with_index()
|> Enum.map(fn {{{card, {x, y, w}}, kind}, index} ->
%{
title: card.title,
note: Map.get(card, :note, ""),
x: x,
y: y,
w: w,
kind: kind,
index: index
}
end)
points = points(assigns.values, 16, 112, 271, 64)
assigns =
assigns
|> assign(:tiles, tiles)
|> assign(:line, Enum.map_join(points, " ", fn {x, y} -> "#{x},#{y}" end))
|> assign(:last, List.last(points))
|> assign(:height, if(tall, do: 300, else: 196))
~H"""
<.frame name="sections/bento" label={@label} {frame_opts(assigns)}>
<svg viewBox={"0 0 460 #{@height}"}>
<g class="ehv-h-lift">
<g class="ehv-a-rise">
<rect
class="ehv-card ehv-sections-bento__tile"
x="0.5"
y="0.5"
width="302"
height="195"
rx="12"
/>
<text class="ehv-mono" x="16" y="27" font-size="8.5">{String.upcase(@eyebrow)}</text>
<text class="ehv-t-2" x="16" y="48" font-size="12">{@title}</text>
<text class="ehv-t-display" x="16" y="88" font-size="34">{@value}</text>
<line
:for={y <- [112.5, 144.5, 176.5]}
class="ehv-line-faint"
x1="16"
x2="287"
y1={y}
y2={y}
/>
<polyline
class="ehv-line-lime ehv-round ehv-a-draw ehv-i-3"
points={@line}
pathLength="1"
/>
<circle
class="ehv-fill-lime ehv-a-ping ehv-i-10"
cx={elem(@last, 0)}
cy={elem(@last, 1)}
r="4"
/>
<circle
class="ehv-fill-lime ehv-a-pop ehv-i-10"
cx={elem(@last, 0)}
cy={elem(@last, 1)}
r="3.5"
/>
</g>
<g :for={tile <- @tiles} class={"ehv-a-rise ehv-i-#{tile.index + 1}"}>
<rect
class="ehv-card ehv-sections-bento__tile"
x={tile.x + 0.5}
y={tile.y + 0.5}
width={tile.w - 1}
height="91"
rx="12"
/>
<text class="ehv-t-strong" x={tile.x + 14} y={tile.y + 24} font-size="11.5">
{tile.title}
</text>
<text class="ehv-mono" x={tile.x + 14} y={tile.y + 38} font-size="7.5">
{String.upcase(tile.note)}
</text>
<g :if={tile.kind == :people}>
<g :for={{initials, index} <- Enum.with_index(~w(MN JK EL))}>
<circle
class="ehv-sections-bento__avatar"
cx={tile.x + 25 + index * 17}
cy={tile.y + 66}
r="11"
/>
<text
class="ehv-mono ehv-t-2"
x={tile.x + 25 + index * 17}
y={tile.y + 68.5}
font-size="6.5"
text-anchor="middle"
>
{initials}
</text>
</g>
<rect
class="ehv-fill-soft"
x={tile.x + 82}
y={tile.y + 64}
width={tile.w - 96}
height="4"
rx="2"
/>
</g>
<g :if={tile.kind == :ring}>
<circle class="ehv-sections-bento__track" cx={tile.x + 27} cy={tile.y + 65} r="11" />
<circle
class="ehv-sections-bento__arc"
cx={tile.x + 27}
cy={tile.y + 65}
r="11"
pathLength="100"
stroke-dasharray="72 100"
transform={"rotate(-90 #{tile.x + 27} #{tile.y + 65})"}
/>
<rect
class="ehv-fill-soft"
x={tile.x + 50}
y={tile.y + 58}
width={tile.w - 64}
height="4"
rx="2"
/>
<rect
class="ehv-fill-soft"
x={tile.x + 50}
y={tile.y + 69}
width={(tile.w - 64) * 0.6}
height="4"
rx="2"
/>
</g>
<g :if={tile.kind == :rows}>
<g :for={{share, index} <- Enum.with_index([1.0, 0.72, 0.86])}>
<circle class="ehv-fill-3" cx={tile.x + 17} cy={tile.y + 54 + index * 12} r="2.5" />
<rect
class="ehv-fill-soft"
x={tile.x + 26}
y={tile.y + 52 + index * 12}
width={Float.round((tile.w - 40) * share, 1)}
height="4"
rx="2"
/>
</g>
</g>
<g :if={tile.kind == :steps}>
<rect
:for={index <- 0..4}
class={if index < 3, do: "ehv-fill-2", else: "ehv-fill-soft"}
x={Float.round(tile.x + 14 + index * ((tile.w - 28 + 6) / 5), 1)}
y={tile.y + 60}
width={Float.round((tile.w - 28 + 6) / 5 - 6, 1)}
height="6"
rx="3"
/>
</g>
</g>
</g>
</svg>
</.frame>
"""
end
# {x, y, width} of every card. The featured tile takes the first two
# columns and rows; the cards fill the third column and the row below.
defp slots(4), do: [{314, 0, 146}, {314, 104, 146}, {0, 208, 146}, {157, 208, 303}]
defp slots(3), do: [{314, 0, 146}, {314, 104, 146}, {0, 208, 460}]
defp slots(_count), do: [{314, 0, 146}, {314, 104, 146}]
# Scales the series into the box at {x, y} of size {w, h}, y axis flipped.
defp points(values, x, y, w, h) do
{min, max} = Enum.min_max(values, fn -> {0, 0} end)
range = if max == min, do: 1, else: max - min
step = w / max(length(values) - 1, 1)
values
|> Enum.with_index()
|> Enum.map(fn {value, index} ->
{Float.round(x + index * step, 1), Float.round(y + h - (value - min) / range * h, 1)}
end)
end
end
assets/css/sections/bento.css
/* sections/bento */
.ehv-sections-bento { --ehv-max-w: 520px; --ehv-iso-scale: 0.82; }
.ehv-sections-bento__avatar { fill: var(--ehv-surface-3); stroke: var(--ehv-surface-1); stroke-width: 2; }
.ehv-sections-bento__track { fill: none; stroke: var(--ehv-border-soft); stroke-width: 4; }
.ehv-sections-bento__arc { fill: none; stroke: var(--ehv-ink-2); stroke-width: 4; stroke-linecap: round; }
.ehv--hover .ehv-sections-bento__tile { transition: stroke 200ms var(--ehv-ease); }
.ehv--hover .ehv-sections-bento__tile:hover { stroke: var(--ehv-ink-3); }