feat: add CSS stacking avatar fallback to all templates (v1.0.49)

Phase 3 frontend harmonization: apply the same avatar fallback pattern
used in ActivityPub to all Microsub templates. A fallback initials span
is always rendered; when the <img> loads, it covers the fallback via
absolute positioning. Broken images are removed by event delegation,
revealing the initials underneath.

Templates updated: item-card, item, actor, author (3 avatar sizes:
40px card, 48px detail, 80px profile). Error delegation script added
to reader layout.

Confab-Link: http://localhost:8080/sessions/bb4a6ec4-b711-48cd-b3d7-942ec2a9851d
This commit is contained in:
Ricardo
2026-03-13 13:53:45 +01:00
parent 465caccc94
commit 1512bcecb2
7 changed files with 130 additions and 37 deletions

View File

@@ -154,15 +154,40 @@
margin-bottom: var(--space-s); margin-bottom: var(--space-s);
} }
.ms-item-card__avatar-wrap {
flex-shrink: 0;
height: 40px;
position: relative;
width: 40px;
}
.ms-item-card__avatar-wrap > img {
position: absolute;
inset: 0;
z-index: 1;
}
.ms-item-card__author-photo { .ms-item-card__author-photo {
border: var(--border-width-thin) solid var(--color-offset); border: var(--border-width-thin) solid var(--color-offset);
border-radius: 50%; border-radius: 50%;
flex-shrink: 0;
height: 40px; height: 40px;
object-fit: cover; object-fit: cover;
width: 40px; width: 40px;
} }
.ms-item-card__author-photo--default {
align-items: center;
background: var(--color-offset);
border-radius: 50%;
color: var(--color-on-offset);
display: flex;
font-size: var(--font-size-s);
font-weight: 600;
height: 40px;
justify-content: center;
width: 40px;
}
.ms-item-card__author-info { .ms-item-card__author-info {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -496,6 +521,19 @@
margin-bottom: var(--space-m); margin-bottom: var(--space-m);
} }
.ms-item__avatar-wrap {
flex-shrink: 0;
height: 48px;
position: relative;
width: 48px;
}
.ms-item__avatar-wrap > img {
position: absolute;
inset: 0;
z-index: 1;
}
.ms-item__author-photo { .ms-item__author-photo {
border-radius: 50%; border-radius: 50%;
height: 48px; height: 48px;
@@ -503,6 +541,19 @@
width: 48px; width: 48px;
} }
.ms-item__author-photo--default {
align-items: center;
background: var(--color-offset);
border-radius: 50%;
color: var(--color-on-offset);
display: flex;
font-size: var(--font-size-m);
font-weight: 600;
height: 48px;
justify-content: center;
width: 48px;
}
.ms-item__author-info { .ms-item__author-info {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -1010,12 +1061,37 @@
gap: var(--space-m); gap: var(--space-m);
} }
.ms-actor-profile__avatar-wrap {
flex-shrink: 0;
height: 80px;
position: relative;
width: 80px;
}
.ms-actor-profile__avatar-wrap > img {
position: absolute;
inset: 0;
z-index: 1;
}
.ms-actor-profile__avatar { .ms-actor-profile__avatar {
border-radius: 50%; border-radius: 50%;
flex-shrink: 0;
object-fit: cover; object-fit: cover;
} }
.ms-actor-profile__avatar--default {
align-items: center;
background: var(--color-offset);
border-radius: 50%;
color: var(--color-on-offset);
display: flex;
font-size: var(--font-size-xl);
font-weight: 600;
height: 80px;
justify-content: center;
width: 80px;
}
.ms-actor-profile__info { .ms-actor-profile__info {
flex: 1; flex: 1;
min-width: 0; min-width: 0;

View File

@@ -1,6 +1,6 @@
{ {
"name": "@rmdes/indiekit-endpoint-microsub", "name": "@rmdes/indiekit-endpoint-microsub",
"version": "1.0.48", "version": "1.0.49",
"description": "Microsub endpoint for Indiekit. Enables subscribing to feeds and reading content using the Microsub protocol.", "description": "Microsub endpoint for Indiekit. Enables subscribing to feeds and reading content using the Microsub protocol.",
"keywords": [ "keywords": [
"indiekit", "indiekit",

View File

@@ -11,13 +11,16 @@
{# Actor profile card #} {# Actor profile card #}
<div class="ms-actor-profile"> <div class="ms-actor-profile">
<div class="ms-actor-profile__header"> <div class="ms-actor-profile__header">
{% if actor.photo %} <div class="ms-actor-profile__avatar-wrap" data-avatar-fallback>
<img src="{{ actor.photo }}" {% if actor.photo %}
alt="" <img src="{{ actor.photo }}"
class="ms-actor-profile__avatar" alt=""
width="80" class="ms-actor-profile__avatar"
height="80"> width="80"
{% endif %} height="80">
{% endif %}
<span class="ms-actor-profile__avatar ms-actor-profile__avatar--default" aria-hidden="true">{{ actor.name[0] | upper if actor.name else "?" }}</span>
</div>
<div class="ms-actor-profile__info"> <div class="ms-actor-profile__info">
<h2 class="ms-actor-profile__name">{{ actor.name }}</h2> <h2 class="ms-actor-profile__name">{{ actor.name }}</h2>
{% if actor.handle %} {% if actor.handle %}
@@ -73,14 +76,17 @@
{# Author #} {# Author #}
{% if item.author %} {% if item.author %}
<div class="ms-item-card__author" style="padding: 12px 16px 0;"> <div class="ms-item-card__author" style="padding: 12px 16px 0;">
{% if item.author.photo %} <div class="ms-item-card__avatar-wrap" data-avatar-fallback>
<img src="{{ item.author.photo }}" {% if item.author.photo %}
alt="" <img src="{{ item.author.photo }}"
class="ms-item-card__author-photo" alt=""
width="40" class="ms-item-card__author-photo"
height="40" width="40"
loading="lazy"> height="40"
{% endif %} loading="lazy">
{% endif %}
<span class="ms-item-card__author-photo ms-item-card__author-photo--default" aria-hidden="true">{{ item.author.name[0] | upper if item.author.name else "?" }}</span>
</div>
<div class="ms-item-card__author-info"> <div class="ms-item-card__author-info">
<span class="ms-item-card__author-name">{{ item.author.name or "Unknown" }}</span> <span class="ms-item-card__author-name">{{ item.author.name or "Unknown" }}</span>
{% if item.author.url %} {% if item.author.url %}

View File

@@ -8,14 +8,17 @@
{% if item.author %} {% if item.author %}
<header class="ms-item__author"> <header class="ms-item__author">
{% if item.author.photo %} <div class="ms-item__avatar-wrap" data-avatar-fallback>
<img src="{{ item.author.photo }}" {% if item.author.photo %}
alt="" <img src="{{ item.author.photo }}"
class="ms-item__author-photo" alt=""
width="48" class="ms-item__author-photo"
height="48" width="48"
loading="lazy"> height="48"
{% endif %} loading="lazy">
{% endif %}
<span class="ms-item__author-photo ms-item__author-photo--default" aria-hidden="true">{{ item.author.name[0] | upper if item.author.name else "?" }}</span>
</div>
<div class="ms-item__author-info"> <div class="ms-item__author-info">
<span class="ms-item__author-name"> <span class="ms-item__author-name">
{% if item.author.url %} {% if item.author.url %}

View File

@@ -9,4 +9,6 @@
{% include "partials/breadcrumbs.njk" %} {% include "partials/breadcrumbs.njk" %}
{% include "partials/view-switcher.njk" %} {% include "partials/view-switcher.njk" %}
{% block reader %}{% endblock %} {% block reader %}{% endblock %}
{# Avatar fallback — remove broken images to reveal initials fallback underneath #}
<script>document.addEventListener("error",function(e){var t=e.target;if(t.tagName==="IMG"&&t.closest("[data-avatar-fallback]"))t.remove()},true)</script>
{% endblock %} {% endblock %}

View File

@@ -1,9 +1,12 @@
{# Author display #} {# Author display #}
{% if author %} {% if author %}
<div class="author"> <div class="author">
{% if author.photo %} <div class="ms-item__avatar-wrap" data-avatar-fallback>
<img src="{{ author.photo }}" alt="" class="author__photo" width="48" height="48" loading="lazy"> {% if author.photo %}
{% endif %} <img src="{{ author.photo }}" alt="" class="author__photo" width="48" height="48" loading="lazy">
{% endif %}
<span class="ms-item__author-photo ms-item__author-photo--default" aria-hidden="true">{{ author.name[0] | upper if author.name else "?" }}</span>
</div>
<div class="author__info"> <div class="author__info">
<span class="author__name"> <span class="author__name">
{% if author.url %} {% if author.url %}

View File

@@ -53,14 +53,17 @@
{# Author #} {# Author #}
{% if item.author %} {% if item.author %}
<div class="ms-item-card__author"> <div class="ms-item-card__author">
{% if item.author.photo %} <div class="ms-item-card__avatar-wrap" data-avatar-fallback>
<img src="{{ item.author.photo }}" {% if item.author.photo %}
alt="" <img src="{{ item.author.photo }}"
class="ms-item-card__author-photo" alt=""
width="40" class="ms-item-card__author-photo"
height="40" width="40"
loading="lazy"> height="40"
{% endif %} loading="lazy">
{% endif %}
<span class="ms-item-card__author-photo ms-item-card__author-photo--default" aria-hidden="true">{{ item.author.name[0] | upper if item.author.name else "?" }}</span>
</div>
<div class="ms-item-card__author-info"> <div class="ms-item-card__author-info">
<span class="ms-item-card__author-name">{{ item.author.name or "Unknown" }}</span> <span class="ms-item-card__author-name">{{ item.author.name or "Unknown" }}</span>
{% if item._source %} {% if item._source %}