{
  "sha": "85cc0e235b9fffb48a4e252ad66ca8f48402445f",
  "redacted": false,
  "message": "Replay team mark when scrolled into view (#411)",
  "author": {
    "login": "nnneeeooo-nnneeeooo",
    "name": "neo",
    "email": "125815713+nnneeeooo-nnneeeooo@users.noreply.github.com"
  },
  "committer": {
    "login": "web-flow",
    "name": "GitHub",
    "email": "noreply@github.com"
  },
  "created_at": "2026-10-26T15:57:56Z",
  "files": [
    {
      "filename": "static/team-logo.js",
      "previous_filename": "",
      "status": "modified",
      "additions": 18,
      "deletions": 15,
      "changes": 33
    },
    {
      "filename": "tests/test_team_logo.js",
      "previous_filename": "",
      "status": "modified",
      "additions": 7,
      "deletions": 10,
      "changes": 17
    }
  ],
  "diff": "diff --git a/static/team-logo.js b/static/team-logo.js\\nindex ebca80a0a..ec27bfe95 100644\\n--- a/static/team-logo.js\\n+++ b/static/team-logo.js\\n@@ -8,7 +8,14 @@\\n   var reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\\n   var timer = 0;\\n   var entryTimer = 0;\\n-  var observer = null;\\n+  var visibilityListener = null;\\n+\\n+  function stopWaiting() {\\n+    if (!visibilityListener) return;\\n+    window.removeEventListener(\"scroll\", visibilityListener);\\n+    window.removeEventListener(\"resize\", visibilityListener);\\n+    visibilityListener = null;\\n+  }\\n \\n   function play(restart) {\\n     if (reduced.matches || document.documentElement.dataset.theme === \"8bit\") return;\\n@@ -34,23 +41,19 @@\\n       if (image) image.addEventListener(\"load\", function () { startOnEntry(event); }, { once: true });\\n       return;\\n     }\\n-    if (observer) observer.disconnect();\\n+    stopWaiting();\\n     window.clearTimeout(entryTimer);\\n     entryTimer = window.setTimeout(function () {\\n-      var box = trigger.getBoundingClientRect();\\n-      var inView = box.bottom > 0 && box.right > 0 &&\\n-        box.top < window.innerHeight && box.left < window.innerWidth;\\n-      if (inView) { play(Boolean(event.persisted)); return; }\\n-      if (\"IntersectionObserver\" in window) {\\n-        observer = new IntersectionObserver(function (entries) {\\n-          if (!entries.some(function (entry) { return entry.isIntersecting; })) return;\\n-          observer.disconnect();\\n-          play(Boolean(event.persisted));\\n-        }, { threshold: .6 });\\n-        observer.observe(trigger);\\n-      } else {\\n+      visibilityListener = function () {\\n+        var box = trigger.getBoundingClientRect();\\n+        if (box.bottom <= 0 || box.right <= 0 ||\\n+            box.top >= window.innerHeight || box.left >= window.innerWidth) return;\\n+        stopWaiting();\\n         play(Boolean(event.persisted));\\n-      }\\n+      };\\n+      window.addEventListener(\"scroll\", visibilityListener, { passive: true });\\n+      window.addEventListener(\"resize\", visibilityListener);\\n+      visibilityListener();\\n     }, 350);\\n   }\\n \\ndiff --git a/tests/test_team_logo.js b/tests/test_team_logo.js\\nindex f920c3258..c2445cb8c 100644\\n--- a/tests/test_team_logo.js\\n+++ b/tests/test_team_logo.js\\n@@ -11,12 +11,12 @@ function setup(theme = \"light\", prefersReducedMotion = false, initiallyVisible =\\n   const timers = new Map();\\n   const listeners = {};\\n   let nextTimer = 0;\\n-  let observer;\\n+  let visible = initiallyVisible;\\n   const image = { complete: true, naturalWidth: 250 };\\n   const trigger = {\\n     offsetWidth: 88,\\n     getBoundingClientRect() {\\n-      return initiallyVisible\\n+      return visible\\n         ? { top: 718, bottom: 800, left: 24, right: 107 }\\n         : { top: 900, bottom: 982, left: 24, right: 107 };\\n     },\\n@@ -28,17 +28,14 @@ function setup(theme = \"light\", prefersReducedMotion = false, initiallyVisible =\\n     addEventListener(name, handler) { listeners[name] = handler; },\\n     querySelector() { return image; },\\n   };\\n-  class IntersectionObserver {\\n-    constructor(callback) { this.callback = callback; observer = this; }\\n-    observe() {}\\n-    disconnect() {}\\n-  }\\n   const window = {\\n-    IntersectionObserver,\\n     innerWidth: 390,\\n     innerHeight: 844,\\n     matchMedia() { return { matches: prefersReducedMotion }; },\\n     addEventListener(name, handler) { listeners[name] = handler; },\\n+    removeEventListener(name, handler) {\\n+      if (listeners[name] === handler) delete listeners[name];\\n+    },\\n     setTimeout(handler) { const id = ++nextTimer; timers.set(id, handler); return id; },\\n     clearTimeout(id) { timers.delete(id); },\\n   };\\n@@ -46,7 +43,7 @@ function setup(theme = \"light\", prefersReducedMotion = false, initiallyVisible =\\n     documentElement: { dataset: { theme } },\\n     querySelector() { return trigger; },\\n   };\\n-  vm.runInNewContext(source, { window, document, IntersectionObserver });\\n+  vm.runInNewContext(source, { window, document });\\n   return {\\n     classes, listeners,\\n     enter(persisted = false) {\\n@@ -56,7 +53,7 @@ function setup(theme = \"light\", prefersReducedMotion = false, initiallyVisible =\\n       timers.delete(first);\\n       handler();\\n     },\\n-    reveal() { observer.callback([{ isIntersecting: true }]); },\\n+    reveal() { visible = true; listeners.scroll(); },\\n     finish() {\\n       for (const handler of timers.values()) handler();\\n       timers.clear();"
}
