From 7825ad2cca7a326bcb0d6d9e59649117e1fcce3b Mon Sep 17 00:00:00 2001 From: Tim McCarthy Date: Sun, 14 Jun 2026 09:39:54 -0700 Subject: [PATCH] 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 --- TODO.md | 2 +- frontend/src/components/EventLog.svelte | 77 +++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/TODO.md b/TODO.md index cab226b..c3c3cdf 100644 --- a/TODO.md +++ b/TODO.md @@ -18,7 +18,7 @@ - [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 diff --git a/frontend/src/components/EventLog.svelte b/frontend/src/components/EventLog.svelte index 1b66d09..6748d74 100644 --- a/frontend/src/components/EventLog.svelte +++ b/frontend/src/components/EventLog.svelte @@ -35,6 +35,11 @@ let atBottom = true; let error = ''; 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). $: canRollback = state.game?.phase === 'scene' @@ -57,20 +62,24 @@ // 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. const version = state.game?.rollback_timeline_version ?? 0; + let reset = false; if (timelineVersion !== null && version !== timelineVersion) { events = []; seenIds = new Set(); + reset = true; } timelineVersion = version; if (events.length === 0) { haveMore = state.events_have_more ?? polled.length >= PAGE_SIZE; } let added = false; + let newest = null; for (const e of polled) { if (!seenIds.has(e.id)) { seenIds.add(e.id); events.push(e); added = true; + newest = e; } } if (added) { @@ -79,7 +88,20 @@ await tick(); 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() { @@ -149,6 +171,19 @@
+ {#if toast && !open} + {#key toast.id} + + {/key} + {/if} @@ -352,6 +387,48 @@ 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 */ .log-kind-challenge { border-left-color: var(--danger); } .log-kind-card { border-left-color: var(--deep); }