email/inbox.ex
Inbox
An inbox list with a sender, subject, preview, unread dot and time per row, and one row marked as handled by an agent.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.email_inbox animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
title |
string | "Inbox" |
|
messages |
list | [%{time: "09:41", from: "Marta Nowak", subject: "Month-end close", preview: "Accruals are ready for review", unread: true}, ...] |
Up to five `%{from, subject, preview, time}`. Add `unread: true` for the dot and `agent: true` for the tag. Keep sender names under 18 characters. Long previews are cut to fit the row. |
agent_tag |
string | "Handled by agent" |
The tag on rows with `agent: true`. Up to 20 characters. |
count |
any | nil |
The line in the header. Left `nil`, it counts the unread messages. |
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/inbox.ex
defmodule EHVisuals.Email.Inbox do
@moduledoc """
An inbox: a row per message with the sender, the subject and a preview, an
unread dot and the time. Rows an agent has dealt with carry a tag.
"""
use EHVisuals.Composition,
name: "email/inbox",
title: "Inbox",
description:
"An inbox list with a sender, subject, preview, unread dot and time per row, and one row marked as handled by an agent."
@messages [
%{
from: "Marta Nowak",
subject: "Month-end close",
preview: "Accruals are ready for review",
time: "09:41",
unread: true
},
%{
from: "Northwind",
subject: "Order PO-2291",
preview: "Can you confirm the delivery date",
time: "09:12",
agent: true
},
%{
from: "Jan Kowalski",
subject: "Invoice 4471",
preview: "Approved, please post it today",
time: "08:47",
unread: true
},
%{
from: "Acme Tools",
subject: "Price list 2027",
preview: "The updated list is attached",
time: "Tue"
}
]
attr(:title, :string, default: "Inbox")
attr(:messages, :list,
default: @messages,
doc:
"Up to five `%{from, subject, preview, time}`. Add `unread: true` for the dot and `agent: true` for the tag. Keep sender names under 18 characters. Long previews are cut to fit the row."
)
attr(:agent_tag, :string,
default: "Handled by agent",
doc: "The tag on rows with `agent: true`. Up to 20 characters."
)
attr(:count, :any,
default: nil,
doc: "The line in the header. Left `nil`, it counts the unread messages."
)
attr(:label, :string,
default: "An inbox with four messages, two unread and one handled by an agent"
)
frame_attrs()
@spec email_inbox(map()) :: Phoenix.LiveView.Rendered.t()
def email_inbox(assigns) do
messages = Enum.take(assigns.messages, 5)
tag_w = Float.round(24 + String.length(assigns.agent_tag) * 4.9, 1)
unread = Enum.count(messages, &Map.get(&1, :unread, false))
rows =
for {message, index} <- Enum.with_index(messages) do
agent = Map.get(message, :agent, false)
limit = 54
%{
index: index,
y: 44 + index * 52,
from: message.from,
initials: initials(message.from),
subject: clip(message.subject, limit),
preview: clip(message.preview, limit - String.length(message.subject) - 3),
time: message.time,
unread: Map.get(message, :unread, false),
agent: agent,
tag_x: Float.round(352 - String.length(message.time) * 5.5 - tag_w, 1)
}
end
assigns =
assigns
|> assign(:rows, rows)
|> assign(:tag_w, tag_w)
|> assign(:summary, assigns.count || "#{unread} unread")
|> assign(:height, 44 + length(rows) * 52 + 6)
~H"""
<.frame name="email/inbox" label={@label} {frame_opts(assigns)}>
<svg viewBox={"0 0 380 #{@height}"}>
<g class="ehv-h-lift">
<rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="379" height={@height - 1} rx="12" />
<g class="ehv-a-fade ehv-i-1">
<text class="ehv-t-strong" x="20" y="28" font-size="14">{@title}</text>
<text class="ehv-mono" x="360" y="27" font-size="8.5" text-anchor="end">
{String.upcase(@summary)}
</text>
<line class="ehv-line-soft" x1="0.5" x2="379.5" y1="43.5" y2="43.5" />
</g>
<g
:for={row <- @rows}
class={["ehv-email-inbox__row", "ehv-a-rise", "ehv-i-#{row.index + 2}"]}
>
<rect class="ehv-email-inbox__bg" x="6" y={row.y + 3} width="368" height="46" rx="8" />
<line
:if={row.index > 0}
class="ehv-line-faint"
x1="62"
x2="360"
y1={row.y + 0.5}
y2={row.y + 0.5}
/>
<circle :if={row.unread} class="ehv-fill-ink" cx="14" cy={row.y + 26} r="2.5" />
<circle class="ehv-card-3" cx="38" cy={row.y + 26} r="12" />
<text
class="ehv-mono ehv-t-2"
x="38"
y={row.y + 29}
font-size="7.5"
text-anchor="middle"
>
{row.initials}
</text>
<text
class={if row.unread, do: "ehv-t-strong", else: "ehv-t-2"}
x="62"
y={row.y + 21.5}
font-size="12.5"
>
{row.from}
</text>
<text
class={["ehv-mono", row.unread && "ehv-t-2"]}
x="360"
y={row.y + 20.5}
font-size="8"
text-anchor="end"
>
{String.upcase(row.time)}
</text>
<text x="62" y={row.y + 39} font-size="11">
<tspan class={if row.unread, do: "ehv-email-inbox__subject", else: "ehv-t-2"}>
{row.subject}
</tspan>
<tspan :if={row.preview != ""} class="ehv-t-3">· {row.preview}</tspan>
</text>
<g :if={row.agent} class={"ehv-a-pop ehv-i-#{row.index + 5}"}>
<rect
class="ehv-fill-lime-soft"
x={row.tag_x}
y={row.y + 9.5}
width={@tag_w}
height="16"
rx="5"
/>
<path
class={"ehv-line-lime ehv-round ehv-a-draw ehv-i-#{row.index + 7}"}
d={"M#{row.tag_x + 7} #{row.y + 17.5} l2.4 2.4 l4.2 -4.8"}
pathLength="1"
/>
<text
class="ehv-mono ehv-t-lime"
x={row.tag_x + @tag_w - 7}
y={row.y + 20}
font-size="7"
text-anchor="end"
>
{String.upcase(@agent_tag)}
</text>
</g>
</g>
</g>
</svg>
</.frame>
"""
end
defp initials(name) do
name |> String.split() |> Enum.take(2) |> Enum.map_join(&String.first/1) |> String.upcase()
end
defp clip(_text, max_chars) when max_chars < 4, do: ""
defp clip(text, max_chars) do
if String.length(text) > max_chars do
String.trim_trailing(String.slice(text, 0, max_chars - 1)) <> "…"
else
text
end
end
end
assets/css/email/inbox.css
/* email/inbox */
.ehv-email-inbox { --ehv-max-w: 420px; }
.ehv svg .ehv-email-inbox__subject { fill: var(--ehv-ink); }
/* A row lights up under the pointer. */
.ehv .ehv-email-inbox__bg { fill: transparent; }
.ehv--hover .ehv-email-inbox__bg { transition: fill 200ms var(--ehv-ease); }
.ehv--hover .ehv-email-inbox__row:hover .ehv-email-inbox__bg { fill: var(--ehv-surface-2); }