empowered.house ↗

sections/bento.ex

Bento section

A bento grid section in miniature: one featured tile and mixed-size cards with abstract contents.

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

<div class="h-96 rounded-xl border border-border">
  <.sections_bento animated />
</div>
default
isometric
three cards · custom copy
two cards · hover
light surface
isometric · fade out · still

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