diff --git a/static/script.js b/static/script.js
index c28ab68..d0b8967 100644
--- a/static/script.js
+++ b/static/script.js
@@ -272,10 +272,37 @@ function initCopyIp(wrap){
return html;
}
- function showAltIp(ip,info){
- section.innerHTML=buildBlock(ip,info);
+ function buildSkeleton(){
+ var html='
';
+ html+='';
+ for(var i=0;i<8;i++){
+ html+='
';
+ html+='
';
+ html+='
';
+ html+='
';
+ }
+ html+='
';
+ return html;
+ }
+
+ function showSkeleton(){
+ section.innerHTML=buildSkeleton();
section.hidden=false;
- initCopyIp(section.querySelector(".ip-copy-wrap"));
+ section.classList.add("alt-visible");
+ }
+
+ function showAltIp(ip,info){
+ section.classList.remove("alt-visible");
+ setTimeout(function(){
+ section.innerHTML=buildBlock(ip,info);
+ initCopyIp(section.querySelector(".ip-copy-wrap"));
+ section.hidden=false;
+ // Force reflow before adding the visible class for transition
+ void section.offsetHeight;
+ section.classList.add("alt-visible");
+ },primaryIsIPv6?300:0);
}
function fetchJson(ip){
@@ -285,8 +312,17 @@ function initCopyIp(wrap){
});
}
+ function hideSkeleton(){
+ section.classList.remove("alt-visible");
+ setTimeout(function(){
+ section.innerHTML="";
+ section.hidden=true;
+ },300);
+ }
+
function detectAltIp(){
if(primaryIsIPv6&&ipv4Domain){
+ showSkeleton();
var controller=new AbortController();
var timeout=setTimeout(function(){controller.abort();},5000);
fetch("https://"+ipv4Domain+"/",{signal:controller.signal})
@@ -297,12 +333,12 @@ function initCopyIp(wrap){
})
.then(function(text){
var ip=text.trim();
- if(!ip||ip.indexOf(":")!==-1)return;
+ if(!ip||ip.indexOf(":")!==-1){hideSkeleton();return;}
return fetchJson(ip).then(function(info){
showAltIp(ip,info);
});
})
- .catch(function(){});
+ .catch(function(){hideSkeleton();});
}else if(!primaryIsIPv6){
fetch("/ip")
.then(function(r){
diff --git a/static/style.css b/static/style.css
index 26b4d2b..8f65d83 100644
--- a/static/style.css
+++ b/static/style.css
@@ -477,6 +477,82 @@ main {
padding: 6px 10px 6px 28px;
}
+#alt-ip-section {
+ opacity: 0;
+ transition: opacity 0.3s ease;
+}
+
+#alt-ip-section.alt-visible {
+ opacity: 1;
+}
+
+.skeleton-block {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.skeleton-bone {
+ background: var(--color-gray5);
+ border-radius: 6px;
+ position: relative;
+ overflow: hidden;
+}
+
+.skeleton-bone::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(
+ 90deg,
+ transparent 0%,
+ var(--color-bg-tertiary) 50%,
+ transparent 100%
+ );
+ animation: skeleton-shimmer 1.5s ease-in-out infinite;
+}
+
+@keyframes skeleton-shimmer {
+ 0% { translate: -100% 0; }
+ 100% { translate: 100% 0; }
+}
+
+.skeleton-ip {
+ inline-size: 260px;
+ block-size: 42px;
+ margin-block: 28px 20px;
+}
+
+.skeleton-table {
+ inline-size: 100%;
+}
+
+.skeleton-row {
+ display: flex;
+ align-items: center;
+ padding-block: 10px;
+ border-block-start: 1px solid var(--color-separator);
+}
+
+.skeleton-row:last-child {
+ border-block-end: 1px solid var(--color-separator);
+}
+
+.skeleton-row-label {
+ inline-size: 42%;
+ padding-inline: 35px 32px;
+}
+
+.skeleton-row-label .skeleton-bone {
+ inline-size: 60px;
+ block-size: 15px;
+}
+
+.skeleton-row-value .skeleton-bone {
+ inline-size: 100px;
+ block-size: 15px;
+}
+
@media (width <= 480px) {
main {
margin-block-start: 40px;