notifications/bell.ex
Bell
A notification bell with an unread badge and the newest notification popping out beside it.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.notifications_bell animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
count |
integer | 3 |
Unread notifications. Zero hides the badge, more than nine shows 9+. |
source |
string | "Approvals" |
Where the notification comes from. |
time |
string | "Now" |
|
title |
string | "PO-2291 needs your approval" |
Up to 32 characters. |
text |
string | "Northwind · €18,400 · over the limit" |
Up to 38 characters. |
action |
string | "Review" |
|
dismiss |
string | "Later" |
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/bell.ex
defmodule EHVisuals.Notifications.Bell do
@moduledoc """
A notification bell with its unread badge, and the newest notification
popping out beside it.
"""
use EHVisuals.Composition,
name: "notifications/bell",
title: "Bell",
description:
"A notification bell with an unread badge and the newest notification popping out beside it."
attr(:count, :integer,
default: 3,
doc: "Unread notifications. Zero hides the badge, more than nine shows 9+."
)
attr(:source, :string, default: "Approvals", doc: "Where the notification comes from.")
attr(:time, :string, default: "Now")
attr(:title, :string, default: "PO-2291 needs your approval", doc: "Up to 32 characters.")
attr(:text, :string,
default: "Northwind · €18,400 · over the limit",
doc: "Up to 38 characters."
)
attr(:action, :string, default: "Review")
attr(:dismiss, :string, default: "Later")
attr(:label, :string,
default: "A bell with three unread notifications and the newest one shown beside it"
)
frame_attrs()
@spec notifications_bell(map()) :: Phoenix.LiveView.Rendered.t()
def notifications_bell(assigns) do
count = max(assigns.count, 0)
action_w = Float.round(24 + String.length(assigns.action) * 6.1, 1)
assigns =
assigns
|> assign(:unread, count)
|> assign(:badge, if(count > 9, do: "9+", else: Integer.to_string(count)))
|> assign(:caption, if(count == 0, do: "All read", else: "#{count} unread"))
|> assign(:action_w, action_w)
|> assign(:dismiss_x, 142 + action_w + 8)
|> assign(:dismiss_w, Float.round(24 + String.length(assigns.dismiss) * 6.1, 1))
|> assign(:time_w, Float.round(String.length(assigns.time) * 5.8 + 2, 1))
~H"""
<.frame name="notifications/bell" label={@label} {frame_opts(assigns)}>
<svg viewBox="0 0 400 172">
<g class="ehv-h-lift">
<g class="ehv-a-pop">
<rect class="ehv-card-3" x="12.5" y="38.5" width="91" height="91" rx="22" />
<g class="ehv-notifications-bell__ring">
<path
class="ehv-notifications-bell__icon ehv-round"
d="M58 63 v-4 M58 63 c-9.5 0 -15 6.8 -15 16 v8.5 l-5.5 8.5 h41 l-5.5 -8.5 v-8.5 c0 -9.2 -5.5 -16 -15 -16 z"
/>
<path
class="ehv-notifications-bell__icon ehv-round"
d="M52.5 101.5 a5.5 5.5 0 0 0 11 0"
/>
</g>
</g>
<g :if={@unread > 0}>
<circle class="ehv-fill-lime ehv-a-ping ehv-i-4" cx="94" cy="47" r="11" />
<g class="ehv-a-pop ehv-i-3">
<circle class="ehv-notifications-bell__badge" cx="94" cy="47" r="12" />
<text
class="ehv-mono ehv-t-on-lime ehv-t-strong"
x="94"
y="50.5"
font-size="10"
text-anchor="middle"
>
{@badge}
</text>
</g>
</g>
<text class="ehv-mono ehv-a-fade ehv-i-2" x="58" y="150" font-size="8" text-anchor="middle">
{String.upcase(@caption)}
</text>
<g class="ehv-a-fade ehv-i-3">
<line class="ehv-line ehv-dash" x1="104" x2="128" y1="84.5" y2="84.5" />
<rect class="ehv-card" x="146.5" y="46.5" width="223" height="116" rx="12" />
<rect class="ehv-card" x="137.5" y="38.5" width="241" height="116" rx="12" />
</g>
<g class="ehv-a-slide ehv-i-4">
<rect
class="ehv-card ehv-notifications-bell__card"
x="128.5"
y="22.5"
width="259"
height="124"
rx="12"
/>
<rect class="ehv-card-3" x="142.5" y="36.5" width="20" height="20" rx="6" />
<rect class="ehv-fill-ink" x="149" y="43" width="7" height="7" rx="2" />
<text class="ehv-mono ehv-t-2" x="171" y="49.5" font-size="8.5">
{String.upcase(@source)}
</text>
<circle class="ehv-fill-lime ehv-a-pulse" cx={368 - @time_w} cy="46.5" r="2.5" />
<text class="ehv-mono ehv-t-lime" x="374" y="49.5" font-size="8.5" text-anchor="end">
{String.upcase(@time)}
</text>
<text class="ehv-t-strong" x="142" y="80" font-size="13.5">{@title}</text>
<text class="ehv-t-2" x="142" y="98" font-size="11.5">{@text}</text>
<rect class="ehv-fill-lime" x="142" y="110" width={@action_w} height="24" rx="6" />
<text
class="ehv-t-strong ehv-t-on-lime"
x={142 + @action_w / 2}
y="126"
font-size="10.5"
text-anchor="middle"
>
{@action}
</text>
<rect
class="ehv-card-2"
x={@dismiss_x + 0.5}
y="110.5"
width={@dismiss_w - 1}
height="23"
rx="6"
/>
<text
class="ehv-t-2"
x={@dismiss_x + @dismiss_w / 2}
y="126"
font-size="10.5"
text-anchor="middle"
>
{@dismiss}
</text>
</g>
</g>
</svg>
</.frame>
"""
end
end
assets/css/notifications/bell.css
/* notifications/bell */
.ehv-notifications-bell { --ehv-max-w: 440px; --ehv-step: 110ms; }
.ehv-notifications-bell__icon { fill: none; stroke: var(--ehv-ink); stroke-width: 1.5; }
/* The badge sits on the corner of the tile, ringed in the canvas colour. */
.ehv-notifications-bell__badge { fill: var(--ehv-lime-fill); stroke: var(--ehv-canvas); stroke-width: 3; }
/* The bell swings from its top, then rests. */
@keyframes ehv-notifications-bell-ring {
0%, 24%, 100% { transform: rotate(0deg); }
4% { transform: rotate(-11deg); }
9% { transform: rotate(9deg); }
14% { transform: rotate(-6deg); }
19% { transform: rotate(3deg); }
}
.ehv-notifications-bell__ring { transform-box: fill-box; transform-origin: 50% 0%; }
.ehv--animated .ehv-notifications-bell__ring {
animation: ehv-notifications-bell-ring 3.6s ease-in-out infinite;
animation-delay: var(--ehv-dur);
}
.ehv--hover .ehv-notifications-bell__card { transition: stroke 200ms var(--ehv-ease); }
.ehv--hover:hover .ehv-notifications-bell__card { stroke: var(--ehv-lime); }
@media (prefers-reduced-motion: reduce) {
.ehv--animated .ehv-notifications-bell__ring { animation: none; }
}