empowered.house ↗

devices/phone.ex

Phone

A phone in outline showing an approval request that someone can approve on the go.

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

<div class="h-96 rounded-xl border border-border">
  <.devices_phone animated />
</div>
default
isometric
approved
custom request · hover
light surface
isometric · fade out · still · no glow

Props

Prop Type Default What it does
agent string "Intake agent" Who asks. Up to 20 characters.
eyebrow string "Approval request"
title string "Purchase order PO-2291" Up to 24 characters.
amount string "€18,400"
details list [%{label: "Supplier", value: "Northwind"}, %{label: "Budget", value: "Within plan"}, ...] Up to four `%{label, value}` rows under the amount.
actions list ["Approve", "Send back"] The two buttons: the lime one first.
approved any nil The confirmation shown after the tap, for example `Approved · posted to the ERP` (up to 28 characters). Left `nil`, the two buttons are shown.
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/devices/phone.ex
phone.ex
defmodule EHVisuals.Devices.Phone do
  @moduledoc """
  A phone drawn in outline with an approval request on its screen: what is
  asked, the amount, a few details and the two buttons.
  """

  use EHVisuals.Composition,
    name: "devices/phone",
    title: "Phone",
    description:
      "A phone in outline showing an approval request that someone can approve on the go."

  @details [
    %{label: "Supplier", value: "Northwind"},
    %{label: "Budget", value: "Within plan"},
    %{label: "Due", value: "14 Nov"}
  ]

  attr(:agent, :string, default: "Intake agent", doc: "Who asks. Up to 20 characters.")
  attr(:eyebrow, :string, default: "Approval request")
  attr(:title, :string, default: "Purchase order PO-2291", doc: "Up to 24 characters.")
  attr(:amount, :string, default: "€18,400")

  attr(:details, :list,
    default: @details,
    doc: "Up to four `%{label, value}` rows under the amount."
  )

  attr(:actions, :list,
    default: ["Approve", "Send back"],
    doc: "The two buttons: the lime one first."
  )

  attr(:approved, :any,
    default: nil,
    doc:
      "The confirmation shown after the tap, for example `Approved · posted to the ERP` (up to 28 characters). Left `nil`, the two buttons are shown."
  )

  attr(:label, :string, default: "A phone showing a purchase order waiting for approval")

  frame_attrs()

  @spec devices_phone(map()) :: Phoenix.LiveView.Rendered.t()
  def devices_phone(assigns) do
    rows = assigns.details |> Enum.take(4) |> Enum.with_index()
    card_h = 76 + length(rows) * 24
    button_y = 82 + card_h + 12
    home_y = if assigns.approved, do: button_y + 36 + 18, else: button_y + 36 + 8 + 32 + 16

    assigns =
      assigns
      |> assign(:rows, rows)
      |> assign(:card_h, card_h)
      |> assign(:button_y, button_y)
      |> assign(:home_y, home_y)
      |> assign(:primary, Enum.at(assigns.actions, 0, "Approve"))
      |> assign(:secondary, Enum.at(assigns.actions, 1, "Send back"))
      |> assign(:height, home_y + 18)

    ~H"""
    <.frame name="devices/phone" label={@label} {frame_opts(assigns)}>
      <svg viewBox={"0 0 240 #{@height}"}>
        <g class="ehv-h-lift">
          <g class="ehv-a-rise">
            <rect class="ehv-fill-strong" x="1" y="84" width="2.5" height="18" rx="1.25" />
            <rect class="ehv-fill-strong" x="1" y="108" width="2.5" height="18" rx="1.25" />
            <rect class="ehv-fill-strong" x="236.5" y="96" width="2.5" height="34" rx="1.25" />
            <rect class="ehv-card" x="3.5" y="0.5" width="233" height={@height - 1} rx="32" />
            <rect class="ehv-well" x="11" y="8" width="218" height={@height - 16} rx="25" />
            <rect class="ehv-fill-surface-3" x="98" y="16" width="44" height="11" rx="5.5" />
            <text class="ehv-mono ehv-t-2" x="30" y="25" font-size="7.5">9:41</text>
            <rect class="ehv-line" x="196.5" y="18.5" width="14" height="7" rx="2" />
            <rect class="ehv-fill-3" x="198" y="20" width="8" height="4" rx="1" />
            <rect class="ehv-fill-strong" x="211.5" y="20.5" width="1.5" height="3" rx="0.75" />
          </g>
          <g class="ehv-a-fade ehv-i-2">
            <rect class="ehv-card-3" x="26" y="44" width="26" height="26" rx="8" />
            <rect class="ehv-fill-lime" x="34.5" y="52.5" width="9" height="9" rx="3" />
            <text class="ehv-t-strong" x="61" y="55" font-size="12">{@agent}</text>
            <text class="ehv-mono" x="61" y="67" font-size="7">{String.upcase(@eyebrow)}</text>
          </g>
          <g class="ehv-a-rise ehv-i-3">
            <rect class="ehv-card" x="20.5" y="82.5" width="199" height={@card_h} rx="12" />
            <text class="ehv-t-strong" x="34" y="106" font-size="13">{@title}</text>
            <text class="ehv-t-display" x="34" y="140" font-size="26">{@amount}</text>
          </g>
          <g :for={{row, index} <- @rows} class={"ehv-a-rise ehv-i-#{index + 4}"}>
            <line
              class="ehv-line-faint"
              x1="34"
              x2="206"
              y1={152.5 + index * 24}
              y2={152.5 + index * 24}
            />
            <text class="ehv-mono" x="34" y={168 + index * 24} font-size="7.5">
              {String.upcase(row.label)}
            </text>
            <text x="206" y={168.5 + index * 24} font-size="11" text-anchor="end">{row.value}</text>
          </g>
          <g :if={!@approved} class="ehv-a-rise ehv-i-8">
            <g class="ehv-devices-phone__primary">
              <rect class="ehv-fill-lime" x="20" y={@button_y} width="200" height="36" rx="11" />
              <text
                class="ehv-t-strong ehv-t-on-lime"
                x="120"
                y={@button_y + 22.5}
                font-size="12.5"
                text-anchor="middle"
              >
                {@primary}
              </text>
            </g>
            <rect class="ehv-line" x="20.5" y={@button_y + 44.5} width="199" height="31" rx="10" />
            <text
              class="ehv-t-2"
              x="120"
              y={@button_y + 64}
              font-size="11.5"
              text-anchor="middle"
            >
              {@secondary}
            </text>
          </g>
          <g :if={@approved} class="ehv-a-rise ehv-i-8">
            <rect class="ehv-fill-lime-soft" x="20" y={@button_y} width="200" height="36" rx="11" />
            <path
              class="ehv-line-lime ehv-round ehv-a-draw ehv-i-10"
              d={"M33 #{@button_y + 18} l3.4 3.4 l6 -6.8"}
              pathLength="1"
            />
            <text class="ehv-t-lime" x="50" y={@button_y + 22} font-size="11.5">{@approved}</text>
          </g>
          <rect
            class="ehv-fill-strong ehv-a-fade ehv-i-9"
            x="88"
            y={@home_y}
            width="64"
            height="3.5"
            rx="1.75"
          />
        </g>
      </svg>
    </.frame>
    """
  end
end
assets/css/devices/phone.css
phone.css
/* devices/phone */
.ehv-devices-phone { --ehv-max-w: 270px; }

/* On hover the approve button comes forward. */
.ehv-devices-phone__primary { transform-box: fill-box; }
.ehv--hover .ehv-devices-phone__primary { transition: transform 200ms var(--ehv-ease); }
.ehv--hover:hover .ehv-devices-phone__primary { transform: translateY(-2px); }