empowered.house ↗

email/compose.ex

Email compose

An email compose window with recipients, a subject, a few body lines, an attachment and a send button.

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

<div class="h-96 rounded-xl border border-border">
  <.email_compose animated />
</div>
default
isometric
two recipients · no attachment
hover · no glow
light surface
isometric · fade out · still

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