ui(devmode): roll the easter-egg countdown into one updating toast

Clicking the LibrePortal logo 6→9 times spawned four separate
"X clicks away from being a developer" notifications stacked on top of
each other — visual noise for a delightful-bonus interaction.

Now the easter egg keeps a single reference to its current toast and
mutates the `.notification-message` text in place on each subsequent
click. When the toast's 10s auto-remove timer expires mid-sequence
(slow clicker) the next click opens a fresh one — same fallback for
the idle-reset path that clears the count after 3s.

`_devToast` now returns the notification element so the easter-egg
handler can grab it; previously it returned undefined, fine for the
one-shot toasts but no longer enough for the rolling-update pattern.

Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
librelad 2026-05-27 16:01:32 +01:00
parent 38b6dccd4a
commit 57d8e82949

View File

@ -89,6 +89,7 @@ class TopbarComponent {
let count = 0;
let resetTimer = null;
let currentToast = null; // single rolling toast — updated in place
document.addEventListener('click', (e) => {
const logo = e.target.closest('.libreportal-logo');
@ -96,7 +97,10 @@ class TopbarComponent {
count++;
if (resetTimer) clearTimeout(resetTimer);
resetTimer = setTimeout(() => { count = 0; }, RESET_AFTER_MS);
resetTimer = setTimeout(() => {
count = 0;
currentToast = null; // next sequence starts a fresh toast
}, RESET_AFTER_MS);
const remaining = TARGET_CLICKS - count;
const devOn = (window.systemConfigs?.CFG_DEV_MODE === 'true');
@ -104,10 +108,22 @@ class TopbarComponent {
const noun = devOn ? 'developer mode' : 'a developer';
if (remaining > 0 && count >= TOAST_FROM) {
this._devToast(`You are ${remaining} click${remaining === 1 ? '' : 's'} away from ${verb} ${noun}.`, 'info');
const msg = `You are ${remaining} click${remaining === 1 ? '' : 's'} away from ${verb} ${noun}.`;
// If the rolling toast is still in the DOM, mutate its message
// text in place instead of stacking another notification. When
// it's been auto-removed (10s lifetime) we open a fresh one.
const msgEl = currentToast && currentToast.parentElement
? currentToast.querySelector('.notification-message')
: null;
if (msgEl) {
msgEl.innerHTML = msg;
} else {
currentToast = this._devToast(msg, 'info');
}
} else if (remaining === 0) {
count = 0;
clearTimeout(resetTimer);
currentToast = null;
this._setDevMode(!devOn);
}
});
@ -160,15 +176,18 @@ class TopbarComponent {
}
// Toast helper — uses the project's notification system if loaded, else
// a quiet console echo so dev-mode UX never blocks page render.
// a quiet console echo so dev-mode UX never blocks page render. Returns
// the notification DOM element when the real system is available, so
// callers (the easter-egg countdown) can mutate the message in place
// instead of stacking new toasts on each click.
_devToast(message, kind = 'info') {
if (window.notificationSystem?.show) {
window.notificationSystem.show(message, kind);
return window.notificationSystem.show(message, kind);
} else if (typeof window.showNotification === 'function') {
window.showNotification(message, kind);
} else {
console.log(`[dev-mode] ${message}`);
return window.showNotification(message, kind);
}
console.log(`[dev-mode] ${message}`);
return null;
}
// Flip CFG_DEV_MODE via the standard config-update task (same path the