devices/phone.ex
Phone
A phone in outline showing an approval request that someone can approve on the go.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.devices_phone animated />
</div>
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
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
/* 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); }