contact messages: unread nudge animation instead of dot/pill badges
Unread state (new reply for visitors, unanswered threads for admins) now animates the trigger button and the corner icon themselves: a scale-up shake every 4.5 s, gated on prefers-reduced-motion, stopping when read (the has-unread class goes away). The static accent dot and the admin count pill are gone.
This commit is contained in:
1 parent
afa6b967e6
commit
d6088b90ff
4 files changed
+35
-73
No files matched your search
+7
-5
@@ -44,10 +44,12 @@ The token is `crypto.randomUUID()` in localStorage
|
||||
its blake3 hash as the thread key. Pages are shared/cached renders, so the
|
||||
thread UI is entirely client-side. A per-browser "seen" timestamp
|
||||
(`pagerite-contact-seen`) and the newest admin reply's timestamp
|
||||
(`pagerite-contact-lastadmin`) drive the unread dot on trigger buttons and
|
||||
the corner ✉️ icon when an admin reply arrives while the panel is closed —
|
||||
including on a fresh page load before any socket activity (pagerite.js
|
||||
reads the pair directly; the bundle keeps it live once loaded).
|
||||
(`pagerite-contact-lastadmin`) drive the unread marker (a periodic
|
||||
scale-and-shake nudge animation, `has-unread` in pagerite.css) on trigger
|
||||
buttons and the corner ✉️ icon when an admin reply arrives while the panel
|
||||
is closed — including on a fresh page load before any socket activity
|
||||
(pagerite.js reads the pair directly; the bundle keeps it live once
|
||||
loaded).
|
||||
|
||||
The panel rides the public `/_ws` activity socket via a bridge in
|
||||
`pagerite.js` (`register`/`send`, sharing its queue and reconnect pacing —
|
||||
@@ -101,7 +103,7 @@ Replies store the SSO `remote-user` name for audit but are labeled with
|
||||
the currently configured **site name** in the wire payload (substituted at
|
||||
read time) and mark the thread read; `unread` on a thread means its last
|
||||
message is the visitor's and newer than the admin's last read. The icon's
|
||||
badge (a count of unanswered threads) is seeded by the `/_api/settings`
|
||||
unread marker (any unanswered thread) is seeded by the `/_api/settings`
|
||||
probe's `contact_unread` on every page load and kept live by the inbox
|
||||
while it is open.
|
||||
|
||||
|
||||
@@ -408,10 +408,9 @@ body.editing #sidebar {
|
||||
}
|
||||
|
||||
/* The corner ✉️ (docs/messages.md) is in the pens row for everyone, so
|
||||
needs its own positioning context for the unread markers: the same
|
||||
accent dot as the trigger buttons (visitors: a new reply) or a count
|
||||
pill (admins: unanswered threads). The marker lifts the icon out of the
|
||||
icon-btn dimming so it reads clearly. */
|
||||
needs its own positioning context. Unread (a new reply for visitors,
|
||||
unanswered threads for admins) lifts it out of the icon-btn dimming
|
||||
and animates it (see contact-nudge by the trigger button). */
|
||||
.contact-icon {
|
||||
position: relative;
|
||||
}
|
||||
@@ -420,34 +419,6 @@ body.editing #sidebar {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.contact-icon.has-unread::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -0.2em;
|
||||
right: -0.25em;
|
||||
width: 0.5em;
|
||||
height: 0.5em;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 0 0.12em var(--bg);
|
||||
}
|
||||
|
||||
.contact-icon .n {
|
||||
position: absolute;
|
||||
top: -0.5em;
|
||||
right: -0.7em;
|
||||
min-width: 1.3em;
|
||||
padding: 0.05em 0.3em;
|
||||
border-radius: 1em;
|
||||
background: var(--accent);
|
||||
color: var(--bg);
|
||||
font-size: 0.62em;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
grid-column: 1;
|
||||
/* Pinned to the page's start edge (not the article's) and kept in view
|
||||
@@ -948,16 +919,24 @@ article {
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.contact-trigger.has-unread::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -0.25em;
|
||||
right: -0.25em;
|
||||
width: 0.6em;
|
||||
height: 0.6em;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 0 0.15em var(--bg);
|
||||
/* Unread (a new admin reply, or unanswered threads on the corner icon —
|
||||
the .has-unread class, set by contact-main): a scale-up with a shake
|
||||
every few seconds to draw the eye, stopping when read (the class goes
|
||||
away). No static badge; reduced-motion users just get the lift above. */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.contact-trigger.has-unread,
|
||||
.contact-icon.has-unread {
|
||||
animation: contact-nudge 4.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes contact-nudge {
|
||||
0%, 16%, 100% { transform: none; }
|
||||
2% { transform: scale(1.15) rotate(-8deg); }
|
||||
5% { transform: scale(1.15) rotate(7deg); }
|
||||
8% { transform: scale(1.15) rotate(-5deg); }
|
||||
11% { transform: scale(1.15) rotate(4deg); }
|
||||
14% { transform: scale(1.12); }
|
||||
}
|
||||
}
|
||||
|
||||
.edit-link {
|
||||
|
||||
@@ -105,32 +105,18 @@ async function onPow(msg) {
|
||||
}
|
||||
}
|
||||
|
||||
// Unread markers: a dot on every contact trigger button while the panel
|
||||
// is closed and the newest admin message is younger than the last look,
|
||||
// and on the corner ✉️ icon — a count of unanswered threads for admins,
|
||||
// the same dot for visitors. Called on thread pushes and (from
|
||||
// pagerite.js) after navigations, whose swap rebuilds the buttons.
|
||||
// Unread markers: the has-unread class (a periodic nudge animation, see
|
||||
// pagerite.css) on every contact trigger button and on the corner ✉️ icon
|
||||
// — a new admin reply for visitors, any unanswered thread for admins.
|
||||
// Called on thread pushes and (from pagerite.js) after navigations, whose
|
||||
// swap rebuilds the buttons.
|
||||
export function refreshBadges() {
|
||||
for (const btn of document.querySelectorAll('.contact-trigger')) {
|
||||
btn.classList.toggle('has-unread', state.unread)
|
||||
}
|
||||
const icon = document.getElementById('contact-icon')
|
||||
if (!icon) return
|
||||
if (state.viewerAdmin) {
|
||||
let badge = icon.querySelector('.n')
|
||||
if (state.adminUnread > 0) {
|
||||
if (!badge) {
|
||||
badge = document.createElement('span')
|
||||
badge.className = 'n'
|
||||
icon.append(badge)
|
||||
}
|
||||
badge.textContent = state.adminUnread
|
||||
} else {
|
||||
badge?.remove()
|
||||
}
|
||||
} else {
|
||||
icon.classList.toggle('has-unread', state.unread)
|
||||
}
|
||||
icon.classList.toggle('has-unread', state.viewerAdmin ? state.adminUnread > 0 : state.unread)
|
||||
}
|
||||
|
||||
function onThread(thread) {
|
||||
|
||||
@@ -199,12 +199,7 @@ import { reconnectPolicy, socketSlot, watchConnecting } from "./reconnect";
|
||||
b.title = admin ? "messages" : "contact us";
|
||||
b.textContent = "✉️";
|
||||
if (admin) {
|
||||
if (adminUnreadSeed > 0) {
|
||||
const badge = document.createElement("span");
|
||||
badge.className = "n";
|
||||
badge.textContent = adminUnreadSeed;
|
||||
b.append(badge);
|
||||
}
|
||||
if (adminUnreadSeed > 0) b.classList.add("has-unread");
|
||||
} else {
|
||||
const seen = Number(localStorage.getItem("pagerite-contact-seen") || 0);
|
||||
const last = localStorage.getItem("pagerite-contact-lastadmin") || "";
|
||||
|
||||
Reference in new issue
Block a user