email/compose.ex
Email compose
An email compose window with recipients, a subject, a few body lines, an attachment and a send button.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.email_compose animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
title |
string | "New message" |
|
to |
list | ["Marta Nowak"] |
One or two recipient names, up to 16 characters each. |
subject |
string | "PO-2291 is ready to approve" |
Up to 38 characters. |
body |
list | ["Hi Marta,", "the purchase order is matched to the contract", "and ready for your approval. Details attached."] |
Up to five lines of about 46 characters. The caret sits on the line after the last. |
attachment |
any | "PO-2291.pdf" |
A file name of up to 28 characters, or `nil` for no attachment. |
send |
string | "Send" |
The button. Up to 16 characters. |
status |
string | "Draft saved" |
The line in the footer. Up to 24 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/email/compose.ex
defmodule EHVisuals.Email.Compose do
@moduledoc """
An email being written: the recipients, the subject, a few lines of body
with the caret on the next line, an attachment and the send button.
"""
use EHVisuals.Composition,
name: "email/compose",
title: "Email compose",
description:
"An email compose window with recipients, a subject, a few body lines, an attachment and a send button."
@body [
"Hi Marta,",
"the purchase order is matched to the contract",
"and ready for your approval. Details attached."
]
attr(:title, :string, default: "New message")
attr(:to, :list,
default: ["Marta Nowak"],
doc: "One or two recipient names, up to 16 characters each."
)
attr(:subject, :string, default: "PO-2291 is ready to approve", doc: "Up to 38 characters.")
attr(:body, :list,
default: @body,
doc: "Up to five lines of about 46 characters. The caret sits on the line after the last."
)
attr(:attachment, :any,
default: "PO-2291.pdf",
doc: "A file name of up to 28 characters, or `nil` for no attachment."
)
attr(:send, :string, default: "Send", doc: "The button. Up to 16 characters.")
attr(:status, :string,
default: "Draft saved",
doc: "The line in the footer. Up to 24 characters."
)
attr(:label, :string,
default: "An email compose window with a recipient, a subject, three lines and a send button"
)
frame_attrs()
@spec email_compose(map()) :: Phoenix.LiveView.Rendered.t()
def email_compose(assigns) do
lines = assigns.body |> Enum.take(5) |> Enum.with_index()
caret_y = 126 + length(lines) * 18
attach_y = caret_y + 12
footer_y = if assigns.attachment, do: attach_y + 38, else: caret_y + 16
{chips, _x} =
assigns.to
|> Enum.take(2)
|> Enum.with_index()
|> Enum.map_reduce(76.0, fn {name, index}, x ->
width = Float.round(36 + String.length(name) * 5.9, 1)
{%{index: index, name: name, initials: initials(name), x: x, w: width}, x + width + 6}
end)
assigns =
assigns
|> assign(:chips, chips)
|> assign(:lines, lines)
|> assign(:caret_y, caret_y)
|> assign(:attach_y, attach_y)
|> assign(:attach_w, Float.round(44 + String.length(assigns.attachment || "") * 5.5, 1))
|> assign(:footer_y, footer_y)
|> assign(:send_w, Float.round(46 + String.length(assigns.send) * 6.3, 1))
|> assign(:height, footer_y + 52)
~H"""
<.frame name="email/compose" label={@label} {frame_opts(assigns)}>
<svg viewBox={"0 0 360 #{@height}"}>
<g class="ehv-h-lift">
<rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="359" height={@height - 1} rx="12" />
<g class="ehv-a-fade ehv-i-1">
<text class="ehv-t-strong" x="20" y="24" font-size="12.5">{@title}</text>
<path class="ehv-line-ink ehv-round" d="M314 20 h8" />
<path class="ehv-line-ink ehv-round" d="M333 16 l7 7 m0 -7 l-7 7" />
<line class="ehv-line-soft" x1="0.5" x2="359.5" y1="36.5" y2="36.5" />
</g>
<g class="ehv-a-fade ehv-i-2">
<text class="ehv-mono" x="20" y="57" font-size="8">TO</text>
<line class="ehv-line-faint" x1="20" x2="340" y1="70.5" y2="70.5" />
</g>
<g :for={chip <- @chips} class={"ehv-a-pop ehv-i-#{chip.index + 3}"}>
<rect class="ehv-card-2" x={chip.x} y="42.5" width={chip.w} height="22" rx="11" />
<circle class="ehv-card-3" cx={chip.x + 11.5} cy="53.5" r="8" />
<text
class="ehv-mono ehv-t-2"
x={chip.x + 11.5}
y="56"
font-size="6"
text-anchor="middle"
>
{chip.initials}
</text>
<text x={chip.x + 25} y="57.5" font-size="11">{chip.name}</text>
</g>
<g class="ehv-a-fade ehv-i-4">
<text class="ehv-mono" x="20" y="90" font-size="8">SUBJECT</text>
<text class="ehv-t-strong" x="76" y="90.5" font-size="12">{@subject}</text>
<line class="ehv-line-faint" x1="20" x2="340" y1="102.5" y2="102.5" />
</g>
<text
:for={{line, index} <- @lines}
class={"ehv-t-2 ehv-a-rise ehv-i-#{index + 5}"}
x="20"
y={126 + index * 18}
font-size="12"
>
{line}
</text>
<g class="ehv-a-fade ehv-i-10">
<rect
class="ehv-fill-lime ehv-email-compose__caret"
x="20"
y={@caret_y - 10.5}
width="1.5"
height="13"
/>
</g>
<g :if={@attachment} class="ehv-a-rise ehv-i-10">
<rect
class="ehv-card-2"
x="20.5"
y={@attach_y + 0.5}
width={@attach_w}
height="25"
rx="7"
/>
<path
class="ehv-line-ink ehv-round"
d={"M30.5 #{@attach_y + 6.5} h5.5 l3.5 3.5 v9.5 h-9 z"}
/>
<path class="ehv-line-ink ehv-round" d={"M36 #{@attach_y + 6.5} v3.5 h3.5"} />
<text class="ehv-mono ehv-t-2" x="47" y={@attach_y + 16} font-size="8">
{String.upcase(@attachment)}
</text>
</g>
<g class="ehv-a-fade ehv-i-11">
<line
class="ehv-line-faint"
x1="0.5"
x2="359.5"
y1={@footer_y + 0.5}
y2={@footer_y + 0.5}
/>
<g class="ehv-email-compose__send">
<rect
class="ehv-fill-lime"
x="20"
y={@footer_y + 12}
width={@send_w}
height="28"
rx="7"
/>
<path
class="ehv-email-compose__plane"
d={"M31 #{@footer_y + 20.5} l12 5.5 l-12 5.5 l2.2 -5.5 z"}
/>
<text
class="ehv-t-strong ehv-t-on-lime"
x="51"
y={@footer_y + 30}
font-size="11.5"
>
{@send}
</text>
</g>
<text class="ehv-mono" x="340" y={@footer_y + 29} font-size="8" text-anchor="end">
{String.upcase(@status)}
</text>
</g>
</g>
</svg>
</.frame>
"""
end
defp initials(name) do
name |> String.split() |> Enum.take(2) |> Enum.map_join(&String.first/1) |> String.upcase()
end
end
assets/css/email/compose.css
/* email/compose */
.ehv-email-compose { --ehv-max-w: 400px; }
/* The caret blinks on the line the writer is on. */
@keyframes ehv-email-compose-blink { 50% { opacity: 0; } }
.ehv--animated .ehv-email-compose__caret {
animation: ehv-email-compose-blink 1.1s steps(1, end) infinite;
animation-delay: 1.6s;
}
.ehv .ehv-email-compose__plane { fill: var(--ehv-on-lime); }
/* On hover the send button comes forward. */
.ehv-email-compose__send { transform-box: fill-box; }
.ehv--hover .ehv-email-compose__send { transition: transform 200ms var(--ehv-ease); }
.ehv--hover:hover .ehv-email-compose__send { transform: translateY(-2px); }