feat: skeleton loaders replace loading text (Release 6)

Animated card-shaped placeholders with shimmer effect shown during
content loading instead of plain "Loading..." text. Applied to reader,
tag timeline, and explore tabs (both first-load and load-more states).

Confab-Link: http://localhost:8080/sessions/e9d666ac-3c90-4298-9e92-9ac9d142bc06
This commit is contained in:
Ricardo
2026-03-03 15:48:59 +01:00
parent 2d2dcaec7d
commit fca1738bd3
6 changed files with 132 additions and 24 deletions

View File

@@ -1595,6 +1595,81 @@
cursor: pointer;
}
/* ==========================================================================
Skeleton Loaders
========================================================================== */
@keyframes ap-skeleton-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.ap-skeleton {
background: linear-gradient(90deg,
var(--color-offset) 25%,
var(--color-background) 50%,
var(--color-offset) 75%);
background-size: 200% 100%;
animation: ap-skeleton-shimmer 1.5s ease-in-out infinite;
border-radius: var(--border-radius-small);
}
.ap-card--skeleton {
pointer-events: none;
}
.ap-card--skeleton .ap-card__author {
display: flex;
align-items: center;
gap: var(--space-s);
}
.ap-skeleton--avatar {
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
flex-shrink: 0;
}
.ap-skeleton-lines {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.ap-skeleton--name {
height: 0.85rem;
width: 40%;
}
.ap-skeleton--handle {
height: 0.7rem;
width: 25%;
}
.ap-skeleton-body {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: var(--space-s);
}
.ap-skeleton--line {
height: 0.75rem;
width: 100%;
}
.ap-skeleton--short {
width: 60%;
}
.ap-skeleton-group {
display: flex;
flex-direction: column;
gap: var(--space-m);
}
/* ==========================================================================
Responsive
========================================================================== */

View File

@@ -1,6 +1,6 @@
{
"name": "@rmdes/indiekit-endpoint-activitypub",
"version": "2.5.4",
"version": "2.5.5",
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
"keywords": [
"indiekit",

View File

@@ -256,10 +256,14 @@
x-data="apInfiniteScroll()"
x-init="init()">
<div class="ap-load-more__sentinel" x-ref="sentinel"></div>
<button class="ap-load-more__btn" @click="loadMore()" :disabled="loading" x-show="!done">
<span x-show="!loading">{{ __("activitypub.reader.pagination.loadMore") }}</span>
<span x-show="loading">{{ __("activitypub.reader.pagination.loading") }}</span>
<button class="ap-load-more__btn" @click="loadMore()" :disabled="loading" x-show="!done && !loading">
{{ __("activitypub.reader.pagination.loadMore") }}
</button>
<div class="ap-skeleton-group" x-show="loading" x-cloak>
{% include "partials/ap-skeleton-card.njk" %}
{% include "partials/ap-skeleton-card.njk" %}
{% include "partials/ap-skeleton-card.njk" %}
</div>
<p class="ap-load-more__done" x-show="done" x-cloak>{{ __("activitypub.reader.pagination.noMore") }}</p>
</div>
{% endif %}
@@ -283,10 +287,12 @@
<template x-if="tab.type === 'instance'">
<div class="ap-explore-instance-panel">
{# Loading spinner — first load, no content yet #}
<div class="ap-explore-tab-loading"
{# Skeleton loaders — first load, no content yet #}
<div class="ap-skeleton-group"
x-show="tabState[tab._id] && tabState[tab._id].loading && !tabState[tab._id].html">
<span class="ap-explore-tab-loading__text">{{ __("activitypub.reader.pagination.loading") }}</span>
{% include "partials/ap-skeleton-card.njk" %}
{% include "partials/ap-skeleton-card.njk" %}
{% include "partials/ap-skeleton-card.njk" %}
</div>
{# Error state with retry #}
@@ -304,7 +310,7 @@
x-html="tabState[tab._id] ? tabState[tab._id].html : ''">
</div>
{# Load more button + loading spinner for subsequent pages #}
{# Load more button + skeleton loaders for subsequent pages #}
<div class="ap-load-more"
x-show="tabState[tab._id] && tabState[tab._id].html && !tabState[tab._id].done">
<button class="ap-load-more__btn"
@@ -313,10 +319,11 @@
:disabled="tabState[tab._id]?.loading">
{{ __("activitypub.reader.pagination.loadMore") }}
</button>
<span class="ap-explore-tab-loading__text"
<div class="ap-skeleton-group"
x-show="tabState[tab._id]?.loading">
{{ __("activitypub.reader.pagination.loading") }}
</span>
{% include "partials/ap-skeleton-card.njk" %}
{% include "partials/ap-skeleton-card.njk" %}
</div>
</div>
{# Empty state — loaded successfully but no posts #}
@@ -347,10 +354,12 @@
x-text="hashtagSourcesLine(tab)"
x-cloak></p>
{# Loading spinner — first load, no content yet #}
<div class="ap-explore-tab-loading"
{# Skeleton loaders — first load, no content yet #}
<div class="ap-skeleton-group"
x-show="tabState[tab._id] && tabState[tab._id].loading && !tabState[tab._id].html">
<span class="ap-explore-tab-loading__text">{{ __("activitypub.reader.pagination.loading") }}</span>
{% include "partials/ap-skeleton-card.njk" %}
{% include "partials/ap-skeleton-card.njk" %}
{% include "partials/ap-skeleton-card.njk" %}
</div>
{# Error state with retry #}
@@ -368,7 +377,7 @@
x-html="tabState[tab._id] ? tabState[tab._id].html : ''">
</div>
{# Load more button + loading spinner for subsequent pages #}
{# Load more button + skeleton loaders for subsequent pages #}
<div class="ap-load-more"
x-show="tabState[tab._id] && tabState[tab._id].html && !tabState[tab._id].done">
<button class="ap-load-more__btn"
@@ -377,10 +386,11 @@
:disabled="tabState[tab._id]?.loading">
{{ __("activitypub.reader.pagination.loadMore") }}
</button>
<span class="ap-explore-tab-loading__text"
<div class="ap-skeleton-group"
x-show="tabState[tab._id]?.loading">
{{ __("activitypub.reader.pagination.loading") }}
</span>
{% include "partials/ap-skeleton-card.njk" %}
{% include "partials/ap-skeleton-card.njk" %}
</div>
</div>
{# Empty state — no instance tabs pinned yet #}

View File

@@ -150,10 +150,14 @@
x-data="apInfiniteScroll()"
x-init="init()">
<div class="ap-load-more__sentinel" x-ref="sentinel"></div>
<button class="ap-load-more__btn" @click="loadMore()" :disabled="loading" x-show="!done">
<span x-show="!loading">{{ __("activitypub.reader.pagination.loadMore") }}</span>
<span x-show="loading">{{ __("activitypub.reader.pagination.loading") }}</span>
<button class="ap-load-more__btn" @click="loadMore()" :disabled="loading" x-show="!done && !loading">
{{ __("activitypub.reader.pagination.loadMore") }}
</button>
<div class="ap-skeleton-group" x-show="loading" x-cloak>
{% include "partials/ap-skeleton-card.njk" %}
{% include "partials/ap-skeleton-card.njk" %}
{% include "partials/ap-skeleton-card.njk" %}
</div>
<p class="ap-load-more__done" x-show="done" x-cloak>{{ __("activitypub.reader.pagination.noMore") }}</p>
</div>
{% endif %}

View File

@@ -75,10 +75,14 @@
x-data="apInfiniteScroll()"
x-init="init()">
<div class="ap-load-more__sentinel" x-ref="sentinel"></div>
<button class="ap-load-more__btn" @click="loadMore()" :disabled="loading" x-show="!done">
<span x-show="!loading">{{ __("activitypub.reader.pagination.loadMore") }}</span>
<span x-show="loading">{{ __("activitypub.reader.pagination.loading") }}</span>
<button class="ap-load-more__btn" @click="loadMore()" :disabled="loading" x-show="!done && !loading">
{{ __("activitypub.reader.pagination.loadMore") }}
</button>
<div class="ap-skeleton-group" x-show="loading" x-cloak>
{% include "partials/ap-skeleton-card.njk" %}
{% include "partials/ap-skeleton-card.njk" %}
{% include "partials/ap-skeleton-card.njk" %}
</div>
<p class="ap-load-more__done" x-show="done">{{ __("activitypub.reader.pagination.noMore") }}</p>
</div>
{% endif %}

View File

@@ -0,0 +1,15 @@
{# Skeleton loading card — animated placeholder while content loads #}
<div class="ap-card ap-card--skeleton" aria-hidden="true">
<header class="ap-card__author">
<div class="ap-skeleton ap-skeleton--avatar"></div>
<div class="ap-skeleton-lines">
<div class="ap-skeleton ap-skeleton--name"></div>
<div class="ap-skeleton ap-skeleton--handle"></div>
</div>
</header>
<div class="ap-skeleton-body">
<div class="ap-skeleton ap-skeleton--line"></div>
<div class="ap-skeleton ap-skeleton--line"></div>
<div class="ap-skeleton ap-skeleton--line ap-skeleton--short"></div>
</div>
</div>