Toast new events into the Event Log button

When the Event Log is collapsed, a freshly-arrived event now flashes as
a toast anchored above the button, then animates down and fades into it.
Suppressed on the initial state load and on rollback reseeds so it only
fires for genuinely new events; clicking the toast opens the log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 09:39:54 -07:00
parent 473fd74a00
commit 7825ad2cca
2 changed files with 78 additions and 1 deletions

View File

@@ -18,7 +18,7 @@
- [x] The back button on the admin page should go back to the active game, not the join screen - [x] The back button on the admin page should go back to the active game, not the join screen
- [ ] Add a toast pop-up for new events entering the event log, that then fades into the event log button - [x] Add a toast pop-up for new events entering the event log, that then fades into the event log button
- [ ] When you get a gat, there should be a pop up to enter a description of it, as with your name - [ ] When you get a gat, there should be a pop up to enter a description of it, as with your name

View File

@@ -35,6 +35,11 @@
let atBottom = true; let atBottom = true;
let error = ''; let error = '';
let timelineVersion = null; let timelineVersion = null;
// Toast that briefly previews a freshly-arrived event while the log is
// collapsed, then animates down into the Event Log button.
let toast = null; // { id, message, kind }
let initialized = false; // suppresses a toast flood on the first state load
// Rollback buttons appear during a scene, for Admins and Deep players (server-enforced too). // Rollback buttons appear during a scene, for Admins and Deep players (server-enforced too).
$: canRollback = state.game?.phase === 'scene' $: canRollback = state.game?.phase === 'scene'
@@ -57,20 +62,24 @@
// action from a rolled-back state truncates the abandoned future and bumps the // action from a rolled-back state truncates the abandoned future and bumps the
// version, which tells us to drop what we'd accumulated and reseed. // version, which tells us to drop what we'd accumulated and reseed.
const version = state.game?.rollback_timeline_version ?? 0; const version = state.game?.rollback_timeline_version ?? 0;
let reset = false;
if (timelineVersion !== null && version !== timelineVersion) { if (timelineVersion !== null && version !== timelineVersion) {
events = []; events = [];
seenIds = new Set(); seenIds = new Set();
reset = true;
} }
timelineVersion = version; timelineVersion = version;
if (events.length === 0) { if (events.length === 0) {
haveMore = state.events_have_more ?? polled.length >= PAGE_SIZE; haveMore = state.events_have_more ?? polled.length >= PAGE_SIZE;
} }
let added = false; let added = false;
let newest = null;
for (const e of polled) { for (const e of polled) {
if (!seenIds.has(e.id)) { if (!seenIds.has(e.id)) {
seenIds.add(e.id); seenIds.add(e.id);
events.push(e); events.push(e);
added = true; added = true;
newest = e;
} }
} }
if (added) { if (added) {
@@ -79,8 +88,21 @@
await tick(); await tick();
scrollToBottom(); scrollToBottom();
} }
// Toast only for genuinely new events (not the initial seed or a
// rollback reseed) and only while the log is closed.
if (initialized && !reset && !open && newest) {
toast = { id: newest.id, message: newest.message, kind: newest.kind || 'info' };
} }
} }
initialized = true;
}
// Opening the log makes the preview redundant.
$: if (open) toast = null;
function clearToast(id) {
if (toast && toast.id === id) toast = null;
}
function scrollToBottom() { function scrollToBottom() {
if (logEl) logEl.scrollTop = logEl.scrollHeight; if (logEl) logEl.scrollTop = logEl.scrollHeight;
@@ -149,6 +171,19 @@
</script> </script>
<div class="floating-event-log {open ? 'open' : ''}"> <div class="floating-event-log {open ? 'open' : ''}">
{#if toast && !open}
{#key toast.id}
<button
class="event-toast log-kind-{toast.kind}"
title="Open the Event Log"
on:click={toggleOpen}
on:animationend={() => clearToast(toast.id)}
>
<span class="log-icon">{KIND_ICONS[toast.kind] || KIND_ICONS.info}</span>
<span class="toast-message">{toast.message}</span>
</button>
{/key}
{/if}
<button class="toggle-log-btn" on:click={toggleOpen}> <button class="toggle-log-btn" on:click={toggleOpen}>
{open ? '⬇️ Hide Log' : '📜 Event Log'} {open ? '⬇️ Hide Log' : '📜 Event Log'}
</button> </button>
@@ -352,6 +387,48 @@
filter: brightness(1.1); filter: brightness(1.1);
} }
/* Toast preview of a fresh event, anchored above the button */
.event-toast {
position: absolute;
bottom: calc(100% + 8px);
right: 0;
width: 320px;
max-width: 80vw;
display: flex;
align-items: center;
gap: 8px;
text-align: left;
padding: 10px 12px;
background: color-mix(in srgb, var(--bg-deep) 95%, transparent);
border: 1px solid var(--edge);
border-left: 4px solid var(--accent);
border-radius: var(--radius-md);
box-shadow: var(--shadow-deep);
backdrop-filter: blur(10px);
color: var(--text);
font-family: var(--font-body);
cursor: pointer;
transform-origin: bottom right;
animation: event-toast-into-button 3.4s ease-in forwards;
}
.event-toast .toast-message {
flex: 1;
font-size: 0.85rem;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
@keyframes event-toast-into-button {
0% { opacity: 0; transform: translateY(-6px) scale(0.96); }
8% { opacity: 1; transform: translateY(0) scale(1); }
75% { opacity: 1; transform: translateY(0) scale(1); }
100% { opacity: 0; transform: translateY(40px) scale(0.4); }
}
@media (prefers-reduced-motion: reduce) {
.event-toast { animation-duration: 2.5s; animation-timing-function: linear; }
}
/* Accent colors by event kind */ /* Accent colors by event kind */
.log-kind-challenge { border-left-color: var(--danger); } .log-kind-challenge { border-left-color: var(--danger); }
.log-kind-card { border-left-color: var(--deep); } .log-kind-card { border-left-color: var(--deep); }