empowered.house ↗

payments/checkout.ex

Checkout

A checkout summary with line items, a total and a pay button.

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

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

Props

Prop Type Default What it does
eyebrow string "Order ORD-4471"
items list [%{name: "Label printer ZT-4", amount: "€1,180.00", detail: "2 pcs"}, %{name: "Thermal labels 100 x 150", ...}, ...] Up to five `%{name, detail, amount}`. Names up to 26 characters.
extras list [%{label: "Subtotal", amount: "€1,537.00"}, %{label: "VAT 23%", amount: "€353.51"}] Up to three `%{label, amount}` between the items and the total. May be empty.
total_label string "Total"
total string "€1,890.51"
button string "Pay €1,890.51" Up to 30 characters.
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/payments/checkout.ex
checkout.ex
defmodule EHVisuals.Payments.Checkout do
  @moduledoc """
  A checkout summary: the line items, what is added on top, the total and the
  button that pays it.
  """

  use EHVisuals.Composition,
    name: "payments/checkout",
    title: "Checkout",
    description: "A checkout summary with line items, a total and a pay button."

  @items [
    %{name: "Label printer ZT-4", detail: "2 pcs", amount: "€1,180.00"},
    %{name: "Thermal labels 100 x 150", detail: "40 rolls", amount: "€312.00"},
    %{name: "Delivery to dock 4", detail: "Northwind, 2 days", amount: "€45.00"}
  ]

  attr(:eyebrow, :string, default: "Order ORD-4471")

  attr(:items, :list,
    default: @items,
    doc: "Up to five `%{name, detail, amount}`. Names up to 26 characters."
  )

  attr(:extras, :list,
    default: [%{label: "Subtotal", amount: "€1,537.00"}, %{label: "VAT 23%", amount: "€353.51"}],
    doc: "Up to three `%{label, amount}` between the items and the total. May be empty."
  )

  attr(:total_label, :string, default: "Total")
  attr(:total, :string, default: "€1,890.51")
  attr(:button, :string, default: "Pay €1,890.51", doc: "Up to 30 characters.")
  attr(:label, :string, default: "A checkout summary with three line items, VAT and a pay button")
  frame_attrs()

  @spec payments_checkout(map()) :: Phoenix.LiveView.Rendered.t()
  def payments_checkout(assigns) do
    items = assigns.items |> Enum.take(5) |> Enum.with_index()
    extras = assigns.extras |> Enum.take(3) |> Enum.with_index()
    extras_y = 48 + length(items) * 38 + 4
    total_y = if extras == [], do: extras_y, else: extras_y + 8 + length(extras) * 22 + 4
    button_y = total_y + 44

    assigns =
      assigns
      |> assign(:rows, items)
      |> assign(:extra_rows, extras)
      |> assign(:count, if(length(items) == 1, do: "1 item", else: "#{length(items)} items"))
      |> assign(:extras_y, extras_y)
      |> assign(:total_y, total_y)
      |> assign(:button_y, button_y)
      |> assign(:height, button_y + 36 + 20)

    ~H"""
    <.frame name="payments/checkout" label={@label} {frame_opts(assigns)}>
      <svg viewBox={"0 0 340 #{@height}"}>
        <g class="ehv-h-lift">
          <rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="339" height={@height - 1} rx="12" />
          <g class="ehv-a-fade ehv-i-1">
            <text class="ehv-mono" x="20" y="31" font-size="9">{String.upcase(@eyebrow)}</text>
            <text class="ehv-mono ehv-t-4" x="320" y="31" font-size="9" text-anchor="end">
              {String.upcase(@count)}
            </text>
          </g>
          <g :for={{item, index} <- @rows} class={"ehv-a-rise ehv-i-#{index + 2}"}>
            <rect class="ehv-card-3" x="20.5" y={53.5 + index * 38} width="27" height="27" rx="7" />
            <rect class="ehv-node-soft" x="29.5" y={62.5 + index * 38} width="9" height="9" rx="2.5" />
            <text x="58" y={65 + index * 38} font-size="12.5">{item.name}</text>
            <text class="ehv-mono" x="58" y={78 + index * 38} font-size="8">
              {String.upcase(item.detail)}
            </text>
            <text x="320" y={71 + index * 38} font-size="12.5" text-anchor="end">{item.amount}</text>
          </g>
          <g class="ehv-a-fade ehv-i-7">
            <line class="ehv-line-faint" x1="20" x2="320" y1={@extras_y + 0.5} y2={@extras_y + 0.5} />
            <g :for={{extra, index} <- @extra_rows}>
              <text class="ehv-t-3" x="20" y={@extras_y + 23 + index * 22} font-size="11.5">
                {extra.label}
              </text>
              <text
                class="ehv-t-2"
                x="320"
                y={@extras_y + 23 + index * 22}
                font-size="11.5"
                text-anchor="end"
              >
                {extra.amount}
              </text>
            </g>
            <line
              :if={@extra_rows != []}
              class="ehv-line-faint"
              x1="20"
              x2="320"
              y1={@total_y + 0.5}
              y2={@total_y + 0.5}
            />
          </g>
          <g class="ehv-a-fade ehv-i-8">
            <text class="ehv-t-strong" x="20" y={@total_y + 28} font-size="13">{@total_label}</text>
            <text
              class="ehv-t-display"
              x="320"
              y={@total_y + 30}
              font-size="20"
              text-anchor="end"
            >
              {@total}
            </text>
          </g>
          <g class="ehv-a-rise ehv-i-9">
            <rect
              class="ehv-fill-lime ehv-payments-checkout__pay"
              x="20"
              y={@button_y}
              width="300"
              height="36"
              rx="8"
            />
            <text
              class="ehv-t-strong ehv-t-on-lime ehv-payments-checkout__pay-text"
              x="170"
              y={@button_y + 22.5}
              font-size="12.5"
              text-anchor="middle"
            >
              {@button}
            </text>
          </g>
        </g>
      </svg>
    </.frame>
    """
  end
end
assets/css/payments/checkout.css
checkout.css
/* payments/checkout */
.ehv-payments-checkout { --ehv-max-w: 370px; }

.ehv-payments-checkout__pay-text { pointer-events: none; }
.ehv--hover .ehv-payments-checkout__pay { transition: fill 200ms var(--ehv-ease); }
.ehv--hover .ehv-payments-checkout__pay:hover { fill: var(--ehv-ink); }