mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-28 08:22:24 +00:00
Merge pull request #93 from PR0M3TH3AN/codex/add-fade-in-effect-to-sidebar-elements
Document fade-in safeguards
This commit is contained in:
5 files changed
+89
-16
No files matched your search
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user