notifications/list.ex
Notification list
A list of notifications with a small icon and a timestamp each, and the unread ones marked.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.notifications_list animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
title |
string | "Notifications" |
|
items |
list | [%{meta: "Northwind · €18,400", time: "Now", title: "PO-2291 needs your approval", icon: "approval", unread: true}, ...] |
Up to six `%{icon, title, meta, time, unread}`. Icon is `approval`, `alert`, `agent`, `clock`, `doc` or `message`; `alert` is drawn in coral. Titles up to 32 characters. |
badge |
any | nil |
The pill in the header. Left empty, it counts the unread items. |
footer |
any | "Mark all as read" |
The line at the bottom, or `nil`. |
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/notifications/list.ex
defmodule EHVisuals.Notifications.List do
@moduledoc """
A list of notifications, each with a small drawn icon and a timestamp, and
the unread ones marked.
"""
use EHVisuals.Composition,
name: "notifications/list",
title: "Notification list",
description:
"A list of notifications with a small icon and a timestamp each, and the unread ones marked."
@items [
%{
icon: "approval",
title: "PO-2291 needs your approval",
meta: "Northwind · €18,400",
time: "Now",
unread: true
},
%{
icon: "alert",
title: "Stock is low in aisle 14",
meta: "Acme Tools · 40 units left",
time: "12 min",
unread: true
},
%{
icon: "agent",
title: "Invoice run finished",
meta: "128 posted to the ERP",
time: "1 h",
unread: false
},
%{
icon: "clock",
title: "Month-end close starts Monday",
meta: "Finance calendar",
time: "Yesterday",
unread: false
}
]
@rows_y 52
@row_h 46
attr(:title, :string, default: "Notifications")
attr(:items, :list,
default: @items,
doc:
"Up to six `%{icon, title, meta, time, unread}`. Icon is `approval`, `alert`, `agent`, `clock`, `doc` or `message`; `alert` is drawn in coral. Titles up to 32 characters."
)
attr(:badge, :any,
default: nil,
doc: "The pill in the header. Left empty, it counts the unread items."
)
attr(:footer, :any, default: "Mark all as read", doc: "The line at the bottom, or `nil`.")
attr(:label, :string,
default: "A list of four notifications with icons and timestamps, two of them unread"
)
frame_attrs()
@spec notifications_list(map()) :: Phoenix.LiveView.Rendered.t()
def notifications_list(assigns) do
rows =
for {item, index} <- assigns.items |> Enum.take(6) |> Enum.with_index() do
%{
index: index,
y: @rows_y + index * @row_h,
icon: Map.get(item, :icon, ""),
title: Map.get(item, :title, ""),
meta: Map.get(item, :meta, ""),
time: Map.get(item, :time, ""),
unread: Map.get(item, :unread, false)
}
end
unread = Enum.count(rows, & &1.unread)
badge = assigns.badge || if(unread > 0, do: "#{unread} new")
footer_y = @rows_y + length(rows) * @row_h
assigns =
assigns
|> assign(:rows, rows)
|> assign(:pill, badge)
|> assign(:pill_w, Float.round(16 + String.length(badge || "") * 5.9, 1))
|> assign(:footer_y, footer_y)
|> assign(:height, footer_y + if(assigns.footer, do: 38, else: 6))
~H"""
<.frame name="notifications/list" label={@label} {frame_opts(assigns)}>
<svg viewBox={"0 0 340 #{@height}"}>
<g class="ehv-h-lift">
<rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="339" height={@height - 1} rx="12" />
<g class="ehv-a-fade ehv-i-1">
<text class="ehv-t-strong" x="20" y="34" font-size="14.5">{@title}</text>
<g :if={@pill}>
<rect
class="ehv-fill-lime-soft"
x={320 - @pill_w}
y="19"
width={@pill_w}
height="20"
rx="6"
/>
<text
class="ehv-mono ehv-t-lime"
x={320 - @pill_w / 2}
y="32.5"
font-size="8.5"
text-anchor="middle"
>
{String.upcase(@pill)}
</text>
</g>
</g>
<g :for={row <- @rows} class={"ehv-a-rise ehv-i-#{row.index + 2}"}>
<line class="ehv-line-faint" x1="20" x2="320" y1={row.y + 0.5} y2={row.y + 0.5} />
<rect
class="ehv-notifications-list__row"
x="8"
y={row.y + 4}
width="324"
height="38"
rx="8"
/>
<rect class="ehv-card-3" x="20.5" y={row.y + 10.5} width="25" height="25" rx="8" />
<g transform={"translate(33 #{row.y + 23})"}>
<path
class={[
"ehv-round",
if(row.icon == "alert", do: "ehv-line-coral", else: "ehv-line-ink")
]}
d={icon(row.icon)}
/>
<rect
:if={row.icon == "agent"}
class="ehv-fill-ink"
x="-2"
y="-2"
width="4"
height="4"
rx="1"
/>
</g>
<text
class={[if(row.unread, do: "ehv-t-strong", else: "ehv-t-2")]}
x="58"
y={row.y + 20.5}
font-size="12.5"
>
{row.title}
</text>
<text class="ehv-mono" x="58" y={row.y + 34} font-size="8">
{String.upcase(row.meta)}
</text>
<text
class={["ehv-mono", if(row.unread, do: "ehv-t-2", else: "ehv-t-4")]}
x="320"
y={row.y + 20}
font-size="8"
text-anchor="end"
>
{String.upcase(row.time)}
</text>
<g :if={row.unread} class={"ehv-a-pop ehv-i-#{row.index + 5}"}>
<circle class="ehv-fill-lime ehv-a-pulse" cx="316.5" cy={row.y + 31} r="3" />
</g>
</g>
<g :if={@footer} class="ehv-a-fade ehv-i-8">
<line class="ehv-line-faint" x1="20" x2="320" y1={@footer_y + 0.5} y2={@footer_y + 0.5} />
<text class="ehv-t-2" x="170" y={@footer_y + 23.5} font-size="11.5" text-anchor="middle">
{@footer}
</text>
</g>
</g>
</svg>
</.frame>
"""
end
# Icons are drawn around {0, 0}, inside a 14 by 14 box.
defp icon("approval"), do: "M-4.6 0.3 l3.1 3.1 l6.1 -6.9"
defp icon("alert"), do: "M0 -6 L6.6 5.2 H-6.6 Z M0 -1.9 v3.2 M0 3.3 v0.2"
defp icon("agent"),
do: "M-3 -6 h6 a3 3 0 0 1 3 3 v6 a3 3 0 0 1 -3 3 h-6 a3 3 0 0 1 -3 -3 v-6 a3 3 0 0 1 3 -3 z"
defp icon("clock"), do: "M0 -6.2 a6.2 6.2 0 1 0 0.01 0 z M0 -3.2 V0.2 l2.5 1.6"
defp icon("doc"),
do: "M-4.6 -6.2 h5.6 l3.6 3.6 v8.8 h-9.2 z M1 -6.2 v3.6 h3.6 M-2 1 h4 M-2 3.6 h4"
defp icon("message"), do: "M-6.2 -5.2 h12.4 v8.2 h-6.6 l-3 3 v-3 h-2.8 z"
defp icon(_other), do: "M-3 0 h6 M0 -3 v6"
end
assets/css/notifications/list.css
/* notifications/list */
.ehv-notifications-list { --ehv-max-w: 370px; --ehv-step: 110ms; }
/* A row has no surface of its own until it is hovered. */
.ehv-notifications-list__row { fill: var(--ehv-surface-2); opacity: 0; }
.ehv--hover .ehv-notifications-list__row { transition: opacity 200ms var(--ehv-ease); }
.ehv--hover .ehv-notifications-list__row:hover { opacity: 1; }