Merge pull request #93 from PR0M3TH3AN/codex/add-fade-in-effect-to-sidebar-elements

Document fade-in safeguards
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-09-25 16:33:22 -04:00
commit 3b6aef5dfd
5 files changed
+89 -16

No files matched your search

+5
View File
@@ -1,3 +1,8 @@
<!--
Sidebar fades in once when hydrated on the client. js/index.js removes the
helper classes after the initial animation to avoid flashes each time we
re-render navigation links.
-->
<aside
id="sidebar"
class="bg-gray-900 text-white transition-transform duration-300 ease-in-out fixed top-0 left-0 w-64 h-screen overflow-y-auto fade-in fade-in-delay-100"
+13 -1
View File
@@ -90,6 +90,12 @@ header img {
}
}
/*
Thumbnails start transparent and fade in the first time they finish loading.
`data-thumbnail-loaded` is cleared by js/index.js once the animation completes
so re-rendering the same element later does not replay the transition and cause
a distracting flash.
*/
[data-video-thumbnail][data-thumbnail-loaded="true"] {
animation: video-thumbnail-fade-in 220ms ease-out forwards;
}
@@ -103,7 +109,13 @@ header img {
}
}
/* Generic fade-in utility for interface elements */
/*
Generic fade-in utility for chrome elements (sidebar, header controls, etc.).
js/index.js listens for the `interface-fade-in` animation finishing and removes
the temporary classes so that once an element has gracefully appeared it stays
opaque even if the DOM is re-used or re-parented later.
*/
.fade-in {
opacity: 0;
animation: interface-fade-in 280ms ease-out forwards;
+18
View File
@@ -59,11 +59,22 @@
md:ml-64 ensures content is shifted on desktop so the pinned sidebar doesn't overlap.
On mobile, we also toggle .sidebar-open to shift the entire content.
-->
<!--
`fade-in` is scrubbed by js/index.js after the first animation so that
re-rendering views does not cause the shell to flicker. Only apply it to
elements that should animate exactly once on initial paint.
-->
<div id="app" class="md:ml-64 px-4 py-8 min-h-screen flex flex-col fade-in">
<!-- Header -->
<header class="mb-8 flex items-center w-full">
<!-- Mobile hamburger button (hidden on md+) -->
<!-- New button -->
<!--
Mobile menu button: uses `fade-in` to match the rest of the chrome.
The JS animation cleanup removes the helper classes once the entrance
finishes to prevent flashes on future sidebar toggles.
-->
<button
id="mobileMenuBtn"
class="md:hidden ml-2 mr-4 flex items-center justify-center w-10 h-10 rounded-full bg-transparent hover:bg-transparent focus:outline-none focus:ring-2 focus:ring-[#0f172a] z-[60] fade-in"
@@ -75,6 +86,7 @@
/>
</button>
<!-- Logo -->
<!-- Logo fades in once on initial load. -->
<img
src="assets/svg/bitvid-logo-light-mode.svg"
alt="BitVid Logo"
@@ -82,6 +94,9 @@
/>
<!-- Buttons on the far right -->
<!-- Header action buttons share the fade-in treatment for consistency. -->
<div class="ml-auto flex items-center space-x-4 fade-in fade-in-delay-200">
<!-- Login Button -->
<button
@@ -141,6 +156,9 @@
<div id="modalContainer"></div>
<!-- Tagline / Slogan -->
<!-- Tagline fade matches the cards. -->
<div class="text-center mb-8 fade-in fade-in-delay-300">
<h2 class="text-2xl font-bold text-gray-500 tracking-wide">
seed. zap. subscribe.
+11 -2
View File
@@ -2476,6 +2476,8 @@ class bitvidApp {
this.loadedThumbnails.get(video.id) || "";
const shouldLazyLoadThumbnail =
escapedThumbnail && previouslyLoadedThumbnail !== escapedThumbnail;
const shouldAnimateThumbnail =
!!escapedThumbnail && previouslyLoadedThumbnail !== escapedThumbnail;
const thumbnailAttrLines = ["data-video-thumbnail=\"true\""];
if (shouldLazyLoadThumbnail) {
thumbnailAttrLines.push(
@@ -2567,8 +2569,15 @@ class bitvidApp {
return;
}
if (thumbnailEl.dataset.thumbnailLoaded !== "true") {
thumbnailEl.dataset.thumbnailLoaded = "true";
if (shouldAnimateThumbnail) {
// Flag the element so CSS runs a one-time fade. js/index.js scrubs
// this attribute in the `animationend` handler so reusing the same
// DOM node later will not re-trigger the animation and flash.
if (thumbnailEl.dataset.thumbnailLoaded !== "true") {
thumbnailEl.dataset.thumbnailLoaded = "true";
}
} else if (thumbnailEl.dataset.thumbnailLoaded) {
delete thumbnailEl.dataset.thumbnailLoaded;
}
this.loadedThumbnails.set(video.id, escapedThumbnail);
+42 -13
View File
@@ -3,31 +3,60 @@
import { trackPageView } from "./analytics.js";
const INTERFACE_FADE_IN_ANIMATION = "interface-fade-in";
const VIDEO_THUMBNAIL_FADE_IN_ANIMATION = "video-thumbnail-fade-in";
const handleInterfaceFadeInComplete = (event) => {
//
// Centralized animation cleanup
// ------------------------------
// We attach a single capture-phase listener for animation events so that any
// chrome element or thumbnail using our fade-in helpers can automatically shed
// the temporary classes/data attributes that trigger the transition. This is
// critical because these DOM nodes frequently persist while the surrounding
// lists re-render; if the classes stick around, future layout shuffles would
// replay the fade and make the UI flicker. Clearing the hooks immediately after
// the first animation keeps the graceful entrance without introducing future
// flashes.
const handleFadeInAnimationComplete = (event) => {
const { animationName, target } = event;
if (animationName !== INTERFACE_FADE_IN_ANIMATION) {
return;
}
if (!(target instanceof HTMLElement)) {
return;
}
if (!target.classList.contains("fade-in")) {
if (animationName === INTERFACE_FADE_IN_ANIMATION) {
if (!target.classList.contains("fade-in")) {
return;
}
// Remove the transient fade-in class so future renders keep the element
// visible. The delays piggyback on `fade-in`, so we strip those too.
target.classList.remove("fade-in");
Array.from(target.classList).forEach((className) => {
if (className.startsWith("fade-in-delay-")) {
target.classList.remove(className);
}
});
return;
}
target.classList.remove("fade-in");
Array.from(target.classList).forEach((className) => {
if (className.startsWith("fade-in-delay-")) {
target.classList.remove(className);
}
});
if (animationName !== VIDEO_THUMBNAIL_FADE_IN_ANIMATION) {
return;
}
if (
target.dataset &&
target.dataset.videoThumbnail === "true" &&
target.dataset.thumbnailLoaded === "true"
) {
// Thumbnails use the same pattern: once the fade finishes we clear the flag
// so a later `load` event (or template reuse) does not restart the
// animation.
delete target.dataset.thumbnailLoaded;
}
};
document.addEventListener("animationend", handleInterfaceFadeInComplete, true);
document.addEventListener("animationcancel", handleInterfaceFadeInComplete, true);
document.addEventListener("animationend", handleFadeInAnimationComplete, true);
document.addEventListener("animationcancel", handleFadeInAnimationComplete, true);
// 1) Load modals (login, application, etc.)
async function loadModal(url) {